From fd6e35fe57f9d80411148b21e80e8762f82dd3b7 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sat, 17 Feb 2024 22:11:55 +0800 Subject: [PATCH 001/103] WIP: Generate stub with nanobind --- .gitignore | 3 +- CMakeLists.txt | 31 +- conanfile.py | 12 +- pyproject.toml | 3 +- scripts/get_deps.py | 7 +- src-python/apngasm_python/__init__.pyi | 1 - .../__init__.pyi => _apngasm_python.pyi} | 642 +++++++----------- src-python/apngasm_python/apngasm.py | 16 +- src-python/apngasm_python/apngasm.pyi | 45 -- 9 files changed, 301 insertions(+), 459 deletions(-) delete mode 100644 src-python/apngasm_python/__init__.pyi rename src-python/apngasm_python/{_apngasm_python/__init__.pyi => _apngasm_python.pyi} (66%) delete mode 100644 src-python/apngasm_python/apngasm.pyi diff --git a/.gitignore b/.gitignore index 186ca55..26cf5ec 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ dist/ .py-build-cmake_cache/ .vscode/ conan_output/ -example/output/ \ No newline at end of file +example/output/ +venv/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 72f472c..cc82b33 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -114,14 +114,23 @@ install(TARGETS _apngasm_python COMPONENT python_modules) # Generate stubs for the Python module -option(WITH_PY_STUBS - "Generate Python stub files (.pyi) for the Python module." On) -if (WITH_PY_STUBS AND NOT CMAKE_CROSSCOMPILING) - include(cmake/NanobindStubgen.cmake) - nanobind_stubgen(_apngasm_python) - add_custom_command(TARGET _apngasm_python POST_BUILD - COMMAND ${PYTHON_EXECUTABLE} patch_stub.py $/_apngasm_python.pyi - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/scripts - USES_TERMINAL) - nanobind_stubgen_install(_apngasm_python ${PY_BUILD_CMAKE_MODULE_NAME}) -endif() \ No newline at end of file +# option(WITH_PY_STUBS +# "Generate Python stub files (.pyi) for the Python module." On) +# if (WITH_PY_STUBS AND NOT CMAKE_CROSSCOMPILING) +# include(cmake/NanobindStubgen.cmake) +# nanobind_stubgen(_apngasm_python) +# add_custom_command(TARGET _apngasm_python POST_BUILD +# COMMAND ${PYTHON_EXECUTABLE} patch_stub.py $/_apngasm_python.pyi +# WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/scripts +# USES_TERMINAL) +# nanobind_stubgen_install(_apngasm_python ${PY_BUILD_CMAKE_MODULE_NAME}) +# endif() + +# nanobind_add_stub( +# apngasm_python_stub +# MODULE apngasm_python +# OUTPUT src-python/apngasm_python/_apngasm_python.pyi +# DEPENDS _apngasm_python +# MARKER_FILE src-python/apngasm_python/py.typed +# VERBOSE +# ) \ No newline at end of file diff --git a/conanfile.py b/conanfile.py index 50c5f64..5a0eace 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 +# type: ignore from conan import ConanFile import shutil from scripts.get_arch import get_arch @@ -9,16 +11,16 @@ class ApngasmRecipe(ConanFile): settings = "os", "compiler", "build_type", "arch" def requirements(self): - self.requires("zlib/1.2.13") # type: ignore - self.requires("libpng/1.6.40") # type: ignore + self.requires("zlib/1.2.13") + self.requires("libpng/1.6.40") self.requires( - "boost/1.75.0" # type: ignore + "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") # type: ignore + self.build_requires("b2/4.10.1") if not shutil.which("cmake"): - self.tool_requires("cmake/[>=3.27]") # type: ignore + self.tool_requires("cmake/[>=3.27]") def build(self): build_type = "Release" diff --git a/pyproject.toml b/pyproject.toml index aef0e92..2636c48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,8 +18,7 @@ Tracker = "https://github.com/laggykiller/apngasm-python/issues" [build-system] # How pip and other frontends should build this project requires = [ "py-build-cmake==0.2.0a7", - "nanobind~=1.8.0", - "nanobind-stubgen==0.1.3", + "nanobind @ git+https://github.com/wjakob/nanobind.git@stubgen", "conan>=2.0", "wheel" ] diff --git a/scripts/get_deps.py b/scripts/get_deps.py index a5000f5..ff7b82f 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -3,7 +3,6 @@ import os import sys import subprocess -import platform import shutil SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -12,9 +11,9 @@ from scripts.get_arch import conan_archs, get_arch -def install_deps(arch): +def install_deps(arch: str): # Use Conan to install dependencies - settings = [] + settings: list[str] = [] if platform.system() == "Windows": settings.append("os=Windows") @@ -35,7 +34,7 @@ def install_deps(arch): if arch: settings.append("arch=" + arch) - build = [] + build: list[str] = [] if platform.system() == "Linux": # Need to compile dependencies if Linux build.append("*") diff --git a/src-python/apngasm_python/__init__.pyi b/src-python/apngasm_python/__init__.pyi deleted file mode 100644 index bda5b5a..0000000 --- a/src-python/apngasm_python/__init__.pyi +++ /dev/null @@ -1 +0,0 @@ -__version__: str diff --git a/src-python/apngasm_python/_apngasm_python/__init__.pyi b/src-python/apngasm_python/_apngasm_python.pyi similarity index 66% rename from src-python/apngasm_python/_apngasm_python/__init__.pyi rename to src-python/apngasm_python/_apngasm_python.pyi index c711beb..bc1af79 100644 --- a/src-python/apngasm_python/_apngasm_python/__init__.pyi +++ b/src-python/apngasm_python/_apngasm_python.pyi @@ -1,55 +1,47 @@ -from __future__ import annotations -import numpy.typing -from typing import Any, Optional, overload, Typing, Sequence -from enum import Enum -from . import _apngasm_python +from numpy.typing import ArrayLike +from typing import Annotated +from typing import Optional +from typing import Sequence +from typing import overload class APNGAsm: - """ - Class representing APNG file, storing APNGFrame(s) and other metadata. - """ + """Class representing APNG file, storing APNGFrame(s) and other metadata.""" + @overload + def __init__(self) -> None: + """Construct an empty APNGAsm object.""" - def __init__(self, frames: list[_apngasm_python.APNGFrame]) -> None: + @overload + def __init__(self, frames: Sequence[apngasm_python._apngasm_python.APNGFrame]) -> None: """ Construct APNGAsm object from an existing vector of apngasm frames. - + :param list[apngasm_python._apngasm_python.APNGFrame] frames: A list of APNGFrame objects. """ - ... - - @overload - def __init__(self) -> None: - """ - Construct an empty APNGAsm object. - """ - ... - - def add_frame(self, frame: _apngasm_python.APNGFrame) -> int: + + def add_frame(self, frame: apngasm_python._apngasm_python.APNGFrame) -> int: """ Adds an APNGFrame object to the frame vector. - + :param frame: The APNGFrame object to be added. :type frame: apngasm_python._apngasm_python.APNGFrame - + :return: The new number of frames/the number of this frame on the frame vector. :rtype: 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 DEFAULT_FRAME_NUMERATOR). :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - + :return: The new number of frames/the number of this frame on the frame vector. :rtype: int """ - ... - - def add_frame_from_rgb(self, pixels_rgb: _apngasm_python.rgb, width: int, height: int, trns_color: _apngasm_python.rgb = 0, delay_num: int = 100, delay_den: int = 1000) -> int: + + def add_frame_from_rgb(self, pixels_rgb: apngasm_python._apngasm_python.rgb, width: int, height: int, trns_color: apngasm_python._apngasm_python.rgb = 0, delay_num: int = 100, delay_den: int = 1000) -> int: """ Adds an APNGFrame object to the vector. Not possible to use in Python. As alternative, @@ -57,20 +49,19 @@ class APNGAsm: First create an empty APNGFrame with frame = APNGFrame(), then set frame.width, frame.height, frame.color_type, frame.pixels, frame.palette, frame.delay_num, frame.delay_den manually. - + :param apngasm_python._apngasm_python.rgb pixels_rgb: The RGB pixel data. :param int width: The width of the pixel data. :param int height: The height of the pixel data. :param apngasm_python._apngasm_python.rgb trns_color: The color [r, g, b] to be treated as transparent. :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - + :return: The new number of frames/the number of this frame on the frame vector. :rtype: int """ - ... - - def add_frame_from_rgba(self, pixels_rgba: _apngasm_python.rgba, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> int: + + def add_frame_from_rgba(self, pixels_rgba: apngasm_python._apngasm_python.rgba, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> int: """ Adds an APNGFrame object to the vector. Not possible to use in Python. As alternative, @@ -78,211 +69,171 @@ class APNGAsm: First create an empty APNGFrame with frame = APNGFrame(), then set frame.width, frame.height, frame.color_type, frame.pixels, frame.palette, frame.delay_num, frame.delay_den manually. - + :param apngasm_python._apngasm_python.rgba pixels_rgba: The RGBA pixel data. :param int width: The width of the pixel data. :param int height: The height of the pixel data. :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - + :return: The new number of frames/the number of this frame on the frame vector. :rtype: int """ - ... - + 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 """ - ... - - def disassemble(self, file_path: str) -> list[_apngasm_python.APNGFrame]: + + def disassemble(self, file_path: str) -> list[apngasm_python._apngasm_python.APNGFrame]: """ Disassembles an APNG file. - + :param str file_path: The file path to the PNG image to be disassembled. - + :return: A vector containing the frames of the disassembled PNG. :rtype: list[apngasm_python._apngasm_python.APNGFrame] """ - ... - - def frame_count(self) -> int: - """ - Returns the number of frames. - - :return: number of frames. - :rtype: int - """ - ... - - def get_frames(self) -> list[_apngasm_python.APNGFrame]: - """ - Returns the frame vector. - - :return: frame vector. - :rtype: list[apngasm_python._apngasm_python.APNGFrame] - """ - ... - - def get_loops(self) -> int: - """ - Returns the loop count. - - :return: loop count. - :rtype: int - """ - ... - - def is_skip_first(self) -> bool: + + def save_pngs(self, output_dir: str) -> bool: """ - Returns the flag of skip first frame. - - :return: flag of skip first frame. + 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 """ - ... - - def load_animation_spec(self, file_path: str) -> list[_apngasm_python.APNGFrame]: + + def load_animation_spec(self, file_path: str) -> list[apngasm_python._apngasm_python.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: https://github.com/Genshin/PhantomStandards - + :param str file_path: The path of JSON or XML file. - + :return: A vector containing the frames :rtype: list[apngasm_python._apngasm_python.APNGFrame] """ - ... - - 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 - """ - ... - + def save_json(self, output_path: str, image_dir: str) -> bool: """ Saves a JSON animation spec file. - + :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 """ - ... - - 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 - """ - ... - + def save_xml(self, output_path: str, image_dir: str) -> bool: """ Saves an XML animation spec file. - + :param str file_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 """ - ... - - def set_apngasm_listener(self, listener: Optional[_apngasm_python.IAPNGAsmListener] = None) -> None: + + def set_apngasm_listener(self, listener: Optional[apngasm_python._apngasm_python.IAPNGAsmListener] = None) -> None: """ Sets a listener. - + :param Optional[apngasm_python._apngasm_python.IAPNGAsmListener] listener: A pointer to the listener object. If the argument is NULL a default APNGAsmListener will be created and assigned. """ - ... - + def set_loops(self, loops: int = 0) -> None: """ Set loop count of animation. - + :param int loops: Loop count of animation. If the argument is 0 a loop count is infinity. """ - ... - + def set_skip_first(self, skip_first: bool) -> None: """ Set flag of skip first frame. - + :param int skip_first: Flag of skip first frame. """ - ... - + + def get_frames(self) -> list[apngasm_python._apngasm_python.APNGFrame]: + """ + Returns the frame vector. + + :return: frame vector. + :rtype: list[apngasm_python._apngasm_python.APNGFrame] + """ + + def get_loops(self) -> int: + """ + Returns the loop count. + + :return: loop count. + :rtype: int + """ + + def is_skip_first(self) -> bool: + """ + Returns the flag of skip first frame. + + :return: flag of skip first frame. + :rtype: bool + """ + + def frame_count(self) -> int: + """ + Returns the number of frames. + + :return: number of frames. + :rtype: int + """ + + 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 + """ + def version(self) -> str: """ Returns the version of APNGAsm. - + :return: the version of APNGAsm. :rtype: str """ - ... - -class APNGFrame: - """ - Class representing a frame in APNG. - """ - def __init__(self, pixels: _apngasm_python.rgba, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> None: - """ - Creates an APNGFrame from a bitmapped array of RBGA pixel data. - Not possible to use in Python. To create APNGFrame from pixel data in memory, - Use create_frame_from_rgb() or create_frame_from_rgba(). Or manually, - First create an empty APNGFrame with frame = APNGFrame(), - then set frame.width, frame.height, frame.color_type, frame.pixels, - frame.palette, frame.delay_num, frame.delay_den manually. - - :param apngasm_python._apngasm_python.rgba pixels: The RGBA pixel data. - :param int width: The width of the pixel data. - :param int height: The height of the pixel data. - :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). - :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - """ - ... - +class APNGFrame: + """Class representing a frame in APNG.""" @overload def __init__(self) -> None: - """ - Creates an empty APNGFrame. - """ - ... - + """Creates an empty APNGFrame.""" + @overload def __init__(self, file_path: str, delay_num: int = 100, delay_den: int = 1000) -> None: """ Creates an APNGFrame from a PNG file. - + :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 DEFAULT_FRAME_NUMERATOR). :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). """ - ... - + @overload - def __init__(self, pixels: _apngasm_python.rgb, width: int, height: int, trns_color: _apngasm_python.rgb, delay_num: int = 100, delay_den: int = 1000) -> None: + def __init__(self, pixels: apngasm_python._apngasm_python.rgb, width: int, height: int, trns_color: apngasm_python._apngasm_python.rgb, delay_num: int = 100, delay_den: int = 1000) -> None: """ Creates an APNGFrame from a bitmapped array of RBG pixel data. Not possible to use in Python. To create APNGFrame from pixel data in memory, @@ -290,7 +241,7 @@ class APNGFrame: First create an empty APNGFrame with frame = APNGFrame(), then set frame.width, frame.height, frame.color_type, frame.pixels, frame.palette, frame.delay_num, frame.delay_den manually. - + :param apngasm_python._apngasm_python.rgb pixels: The RGB pixel data. :param int width: The width of the pixel data. :param int height: The height of the pixel data. @@ -298,308 +249,235 @@ class APNGFrame: :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). """ - ... - - @property - def color_type(self) -> int: + + @overload + def __init__(self, pixels: apngasm_python._apngasm_python.rgba, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> None: """ - The color_type of the frame. - - 0: Grayscale (Pillow mode='L') - 2: RGB (Pillow mode='RGB') - 3: Palette (Pillow mode='P') - 4: Grayscale + Alpha (Pillow mode='LA') - 6: RGBA (Pillow mode='RGBA') + Creates an APNGFrame from a bitmapped array of RBGA pixel data. + Not possible to use in Python. To create APNGFrame from pixel data in memory, + Use create_frame_from_rgb() or create_frame_from_rgba(). Or manually, + First create an empty APNGFrame with frame = APNGFrame(), + then set frame.width, frame.height, frame.color_type, frame.pixels, + frame.palette, frame.delay_num, frame.delay_den manually. + + :param apngasm_python._apngasm_python.rgba pixels: The RGBA pixel data. + :param int width: The width of the pixel data. + :param int height: The height of the pixel data. + :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). + :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). """ - ... - @color_type.setter - def color_type(self, arg: int, /) -> None: + + def save(self, out_path: str) -> bool: """ - The color_type of the frame. - - 0: Grayscale (Pillow mode='L') - 2: RGB (Pillow mode='RGB') - 3: Palette (Pillow mode='P') - 4: Grayscale + Alpha (Pillow mode='LA') - 6: RGBA (Pillow mode='RGBA') + Saves this frame as a single PNG file. + + :param str out_path: The relative or absolute path to save the image file to. + + :return: true if save was successful. + :rtype: bool """ - ... - + @property - def delay_den(self) -> int: + def pixels(self) -> Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, None))]: """ - The denominator of the duration of frame. Duration of time is delay_num / delay_den seconds. - """ - ... - @delay_den.setter - def delay_den(self, arg: int, /) -> None: - """ - The denominator of the duration of frame. Duration of time is delay_num / delay_den seconds. + The raw pixel data of frame, expressed as a 3D numpy array in Python. + Note that setting this value will also set the variable 'rows' internally. + This should be set AFTER you set the width, height and color_type. """ - ... - + + @pixels.setter + def pixels(self, arg: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, None))], /) -> None: ... + @property - def delay_num(self) -> int: - """ - The nominator of the duration of frame. Duration of time is delay_num / delay_den seconds. - """ - ... - @delay_num.setter - def delay_num(self, arg: int, /) -> None: - """ - The nominator of the duration of frame. Duration of time is delay_num / delay_den seconds. - """ - ... - + def width(self) -> int: + """The width of frame.""" + + @width.setter + def width(self, arg: int, /) -> None: ... + @property def height(self) -> int: - """ - The height of frame. - """ - ... + """The height of frame.""" + @height.setter - def height(self, arg: int, /) -> None: + def height(self, arg: int, /) -> None: ... + + @property + def color_type(self) -> int: """ - The height of frame. + The color_type of the frame. + + 0: Grayscale (Pillow mode='L') + 2: RGB (Pillow mode='RGB') + 3: Palette (Pillow mode='P') + 4: Grayscale + Alpha (Pillow mode='LA') + 6: RGBA (Pillow mode='RGBA') """ - ... - + + @color_type.setter + def color_type(self, arg: int, /) -> None: ... + @property - def palette(self) -> numpy.typing.NDArray: + def palette(self) -> Annotated[ArrayLike, dict(dtype='uint8', shape=(256, 3))]: """ 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]] in Python. """ - ... + @palette.setter - def palette(self, arg: numpy.typing.NDArray, /) -> None: - """ - 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]] in Python. - """ - ... - - @property - def palette_size(self) -> int: - """ - The palette data size of frame. - """ - ... - @palette_size.setter - def palette_size(self, arg: int, /) -> None: - """ - The palette data size of frame. - """ - ... - - @property - def pixels(self) -> numpy.typing.NDArray: - """ - The raw pixel data of frame, expressed as a 3D numpy array in Python. - Note that setting this value will also set the variable 'rows' internally. - This should be set AFTER you set the width, height and color_type. - """ - ... - @pixels.setter - def pixels(self, arg: numpy.typing.NDArray, /) -> None: - """ - The raw pixel data of frame, expressed as a 3D numpy array in Python. - Note that setting this value will also set the variable 'rows' internally. - This should be set AFTER you set the width, height and color_type. - """ - ... - - def save(self, out_path: str) -> bool: - """ - Saves this frame as a single PNG file. - - :param str out_path: The relative or absolute path to save the image file to. - - :return: true if save was successful. - :rtype: bool - """ - ... - + def palette(self, arg: Annotated[ArrayLike, dict(dtype='uint8', shape=(256, 3))], /) -> None: ... + @property - def transparency(self) -> numpy.typing.NDArray: + def transparency(self) -> Annotated[ArrayLike, dict(dtype='uint8', shape=(None))]: """ The transparency color of frame that is treated as transparent, 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 """ - ... + @transparency.setter - def transparency(self, arg: numpy.typing.NDArray, /) -> None: - """ - The transparency color of frame that is treated as transparent, 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 - """ - ... - + def transparency(self, arg: Annotated[ArrayLike, dict(dtype='uint8', shape=(None))], /) -> None: ... + + @property + def palette_size(self) -> int: + """The palette data size of frame.""" + + @palette_size.setter + def palette_size(self, arg: int, /) -> None: ... + @property def transparency_size(self) -> int: - """ - The transparency data size of frame. - """ - ... + """The transparency data size of frame.""" + @transparency_size.setter - def transparency_size(self, arg: int, /) -> None: - """ - The transparency data size of frame. - """ - ... - + def transparency_size(self, arg: int, /) -> None: ... + @property - def width(self) -> int: - """ - The width of frame. - """ - ... - @width.setter - def width(self, arg: int, /) -> None: + def delay_num(self) -> int: """ - The width of frame. + The nominator of the duration of frame. Duration of time is delay_num / delay_den seconds. """ - ... - -class IAPNGAsmListener: - """ - Class for APNGAsmListener. Meant to be used internally. - """ - def __init__(*args, **kwargs): + @delay_num.setter + def delay_num(self, arg: int, /) -> None: ... + + @property + def delay_den(self) -> int: """ - Initialize self. See help(type(self)) for accurate signature. + The denominator of the duration of frame. Duration of time is delay_num / delay_den seconds. """ - ... - -def create_frame_from_rgb(pixels: numpy.typing.NDArray, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> _apngasm_python.APNGFrame: + + @delay_den.setter + def delay_den(self, arg: int, /) -> None: ... + +class IAPNGAsmListener: + """Class for APNGAsmListener. Meant to be used internally.""" +def create_frame_from_rgb(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 3))], width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> apngasm_python._apngasm_python.APNGFrame: """ Creates an APNGFrame from a bitmapped array of RBG pixel data. - + :param numpy.typing.NDArray pixels: The RGB pixel data, expressed as 3D numpy array. :param int width: The width of the pixel data. :param int height: The height of the pixel data. :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - + :return: A APNGFrame object. :rtype: apngasm_python._apngasm_python.APNGFrame """ - ... -def create_frame_from_rgb_trns(pixels: numpy.typing.NDArray, width: int, height: int, trns_color: numpy.typing.NDArray, delay_num: int = 100, delay_den: int = 1000) -> _apngasm_python.APNGFrame: +def create_frame_from_rgb_trns(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 3))], width: int, height: int, trns_color: Annotated[ArrayLike, dict(dtype='uint8', shape=(3))], delay_num: int = 100, delay_den: int = 1000) -> apngasm_python._apngasm_python.APNGFrame: """ Creates an APNGFrame from a bitmapped array of RBG pixel data, with one color treated as transparent. - + :param numpy.typing.NDArray pixels: The RGB pixel data, expressed as 3D numpy array. :param int width: The width of the pixel data. :param int height: The height of the pixel data. :param numpy.typing.NDArray trns_color: The color [r, g, b] to be treated as transparent, expressed as 1D numpy array. :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - + :return: A APNGFrame object. :rtype: apngasm_python._apngasm_python.APNGFrame """ - ... -def create_frame_from_rgba(pixels: numpy.typing.NDArray, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> _apngasm_python.APNGFrame: +def create_frame_from_rgba(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 4))], width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> apngasm_python._apngasm_python.APNGFrame: """ Creates an APNGFrame from a bitmapped array of RBGA pixel data. - + :param numpy.typing.NDArray pixels: The RGBA pixel data, expressed as 3D numpy array. :param int width: The width of the pixel data. :param int height: The height of the pixel data. :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR) :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR) - + :return: A APNGFrame object. :rtype: apngasm_python._apngasm_python.APNGFrame """ - ... class rgb: - """ - Class for RGB object. Meant to be used internally. - """ - - def __init__(self, arg0: int, arg1: int, arg2: int, /) -> None: - """ - Create a RGB object. Meant to be used internally. - """ - ... - + """Class for RGB object. Meant to be used internally.""" @overload def __init__(self) -> None: - """ - Create an empty RGB object. Meant to be used internally. - """ - ... - + """Create an empty RGB object. Meant to be used internally.""" + + @overload + def __init__(self, arg0: int, arg1: int, arg2: int, /) -> None: + """Create a RGB object. Meant to be used internally.""" + @property - def b(self) -> int: - ... - @b.setter - def b(self, arg: int, /) -> None: - ... - + def r(self) -> int: ... + + @r.setter + def r(self, arg: int, /) -> None: ... + @property - def g(self) -> int: - ... + def g(self) -> int: ... + @g.setter - def g(self, arg: int, /) -> None: - ... - + def g(self, arg: int, /) -> None: ... + @property - def r(self) -> int: - ... - @r.setter - def r(self, arg: int, /) -> None: - ... - -class rgba: - """ - Class for RGBA object. Meant to be used internally. - """ + def b(self) -> int: ... - def __init__(self, arg0: int, arg1: int, arg2: int, arg3: int, /) -> None: - """ - Create a RGBA object. Meant to be used internally. - """ - ... - + @b.setter + def b(self, arg: int, /) -> None: ... + +class rgba: + """Class for RGBA object. Meant to be used internally.""" @overload def __init__(self) -> None: - """ - Create an empty RGBA object. Meant to be used internally. - """ - ... - - @property - def a(self) -> int: - ... - @a.setter - def a(self, arg: int, /) -> None: - ... - + """Create an empty RGBA object. Meant to be used internally.""" + + @overload + def __init__(self, arg0: int, arg1: int, arg2: int, arg3: int, /) -> None: + """Create a RGBA object. Meant to be used internally.""" + @property - def b(self) -> int: - ... - @b.setter - def b(self, arg: int, /) -> None: - ... - + def r(self) -> int: ... + + @r.setter + def r(self, arg: int, /) -> None: ... + @property - def g(self) -> int: - ... + def g(self) -> int: ... + @g.setter - def g(self, arg: int, /) -> None: - ... - + def g(self, arg: int, /) -> None: ... + @property - def r(self) -> int: - ... - @r.setter - def r(self, arg: int, /) -> None: - ... - + def b(self) -> int: ... + + @b.setter + def b(self, arg: int, /) -> None: ... + + @property + def a(self) -> int: ... + + @a.setter + def a(self, arg: int, /) -> None: ... + +del overload +del Sequence +del Optional +del Annotated +del ArrayLike \ No newline at end of file diff --git a/src-python/apngasm_python/apngasm.py b/src-python/apngasm_python/apngasm.py index 1f00a47..73c7f67 100755 --- a/src-python/apngasm_python/apngasm.py +++ b/src-python/apngasm_python/apngasm.py @@ -25,7 +25,7 @@ except ModuleNotFoundError: PILLOW_LOADED = False -from typing import Optional +from typing import Optional, Any class APNGAsmBinder: @@ -42,7 +42,7 @@ def __init__(self): def __enter__(self): return self - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self, exc_type, exc_val, exc_tb): # type: ignore self.apngasm.reset() if PILLOW_LOADED: @@ -77,8 +77,8 @@ def frame_pixels_as_pillow( if NUMPY_LOADED: def frame_pixels_as_numpy( - self, frame: int, new_value: Optional[numpy.typing.NDArray] = None - ) -> Optional[numpy.typing.NDArray]: + self, frame: int, new_value: Optional[numpy.typing.NDArray[Any]] = None + ) -> Optional[numpy.typing.NDArray[Any]]: """ 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. @@ -156,8 +156,8 @@ def frame_color_type( if NUMPY_LOADED: def frame_palette( - self, frame: int, new_value: Optional[numpy.typing.NDArray] = None - ) -> Optional[numpy.typing.NDArray]: + self, frame: int, new_value: Optional[numpy.typing.NDArray[Any]] = None + ) -> Optional[numpy.typing.NDArray[Any]]: """ 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]] @@ -175,8 +175,8 @@ def frame_palette( return self.apngasm.get_frames()[frame].palette def frame_transparency( - self, frame: int, new_value: Optional[numpy.typing.NDArray] = None - ) -> Optional[numpy.typing.NDArray]: + self, frame: int, new_value: Optional[numpy.typing.NDArray[Any]] = None + ) -> Optional[numpy.typing.NDArray[Any]]: """ 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 diff --git a/src-python/apngasm_python/apngasm.pyi b/src-python/apngasm_python/apngasm.pyi deleted file mode 100644 index f175464..0000000 --- a/src-python/apngasm_python/apngasm.pyi +++ /dev/null @@ -1,45 +0,0 @@ -import numpy.typing -from ._apngasm_python import APNGAsm as APNGAsm, APNGFrame as APNGFrame, IAPNGAsmListener as IAPNGAsmListener, __version__ as __version__, create_frame_from_rgb as create_frame_from_rgb, create_frame_from_rgb_trns as create_frame_from_rgb_trns, create_frame_from_rgba as create_frame_from_rgba -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 - def __init__(self) -> None: ... - def __enter__(self): ... - def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: types.TracebackType | None) -> None: ... - def frame_pixels_as_pillow(self, frame: int, new_value: Optional[Image.Image] = None) -> Optional[Image.Image]: ... - def frame_pixels_as_numpy(self, frame: int, new_value: Optional[numpy.typing.NDArray] = None) -> Optional[numpy.typing.NDArray]: ... - def frame_width(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: ... - def frame_height(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: ... - def frame_color_type(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: ... - def frame_palette(self, frame: int, new_value: Optional[numpy.typing.NDArray] = None) -> Optional[numpy.typing.NDArray]: ... - def frame_transparency(self, frame: int, new_value: Optional[numpy.typing.NDArray] = None) -> Optional[numpy.typing.NDArray]: ... - def frame_palette_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]: ... - def frame_delay_num(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: ... - def frame_delay_den(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: ... - def add_frame_from_file(self, file_path: str, delay_num: int = 100, delay_den: int = 1000) -> int: ... - def add_frame_from_pillow(self, pillow_image: Image.Image, delay_num: int = 100, delay_den: int = 1000) -> int: ... - 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: ... - def assemble(self, output_path: str) -> bool: ... - def disassemble_as_numpy(self, file_path: str) -> list[APNGFrame]: ... - def disassemble_as_pillow(self, file_path: str) -> list[APNGFrame]: ... - def save_pngs(self, output_dir: str) -> bool: ... - def load_animation_spec(self, file_path: str) -> list[APNGFrame]: ... - def save_json(self, output_path: str, image_dir: str) -> bool: ... - def save_xml(self, output_path: str, image_dir: str) -> bool: ... - def set_apng_asm_listener(self, listener: Optional[IAPNGAsmListener] = None): ... - def set_loops(self, loops: int = 0): ... - def set_skip_first(self, skip_first: bool): ... - def get_frames(self) -> list[APNGFrame]: ... - def get_loops(self) -> int: ... - def is_skip_first(self) -> int: ... - def frame_count(self) -> int: ... - def reset(self) -> int: ... - def version(self) -> str: ... From aa8180151ceb9c49545893a73fbdda94c9cb8adc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 17 Feb 2024 14:17:58 +0000 Subject: [PATCH 002/103] Auto stub generation --- src-python/apngasm_python/__init__.pyi | 1 + src-python/apngasm_python/apngasm.pyi | 45 ++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src-python/apngasm_python/__init__.pyi create mode 100644 src-python/apngasm_python/apngasm.pyi diff --git a/src-python/apngasm_python/__init__.pyi b/src-python/apngasm_python/__init__.pyi new file mode 100644 index 0000000..bda5b5a --- /dev/null +++ b/src-python/apngasm_python/__init__.pyi @@ -0,0 +1 @@ +__version__: str diff --git a/src-python/apngasm_python/apngasm.pyi b/src-python/apngasm_python/apngasm.pyi new file mode 100644 index 0000000..98fef66 --- /dev/null +++ b/src-python/apngasm_python/apngasm.pyi @@ -0,0 +1,45 @@ +import numpy.typing +from ._apngasm_python import APNGAsm as APNGAsm, APNGFrame as APNGFrame, IAPNGAsmListener as IAPNGAsmListener, __version__ as __version__, create_frame_from_rgb as create_frame_from_rgb, create_frame_from_rgb_trns as create_frame_from_rgb_trns, create_frame_from_rgba as create_frame_from_rgba +from PIL import Image +from _typeshed import Incomplete +from typing import Any, Optional + +NUMPY_LOADED: bool +PILLOW_LOADED: bool + +class APNGAsmBinder: + color_type_dict: Incomplete + apngasm: Incomplete + def __init__(self) -> None: ... + def __enter__(self): ... + def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: types.TracebackType | None) -> None: ... + def frame_pixels_as_pillow(self, frame: int, new_value: Optional[Image.Image] = None) -> Optional[Image.Image]: ... + def frame_pixels_as_numpy(self, frame: int, new_value: Optional[numpy.typing.NDArray[Any]] = None) -> Optional[numpy.typing.NDArray[Any]]: ... + def frame_width(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: ... + def frame_height(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: ... + def frame_color_type(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: ... + def frame_palette(self, frame: int, new_value: Optional[numpy.typing.NDArray[Any]] = None) -> Optional[numpy.typing.NDArray[Any]]: ... + def frame_transparency(self, frame: int, new_value: Optional[numpy.typing.NDArray[Any]] = None) -> Optional[numpy.typing.NDArray[Any]]: ... + def frame_palette_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]: ... + def frame_delay_num(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: ... + def frame_delay_den(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: ... + def add_frame_from_file(self, file_path: str, delay_num: int = 100, delay_den: int = 1000) -> int: ... + def add_frame_from_pillow(self, pillow_image: Image.Image, delay_num: int = 100, delay_den: int = 1000) -> int: ... + 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: ... + def assemble(self, output_path: str) -> bool: ... + def disassemble_as_numpy(self, file_path: str) -> list[APNGFrame]: ... + def disassemble_as_pillow(self, file_path: str) -> list[APNGFrame]: ... + def save_pngs(self, output_dir: str) -> bool: ... + def load_animation_spec(self, file_path: str) -> list[APNGFrame]: ... + def save_json(self, output_path: str, image_dir: str) -> bool: ... + def save_xml(self, output_path: str, image_dir: str) -> bool: ... + def set_apng_asm_listener(self, listener: Optional[IAPNGAsmListener] = None): ... + def set_loops(self, loops: int = 0): ... + def set_skip_first(self, skip_first: bool): ... + def get_frames(self) -> list[APNGFrame]: ... + def get_loops(self) -> int: ... + def is_skip_first(self) -> int: ... + def frame_count(self) -> int: ... + def reset(self) -> int: ... + def version(self) -> str: ... From d8a23ffd6d3724c14072dd8cb9efc6735c3677e8 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sat, 17 Feb 2024 22:38:38 +0800 Subject: [PATCH 003/103] WIP: Generate stub with nanobind --- .github/workflows/update_stub.yml | 27 ---------------- CMakeLists.txt | 15 +-------- pyproject.toml | 3 -- scripts/patch_stub.py | 24 -------------- scripts/update_stub.py | 39 ---------------------- src-python/apngasm_python/__init__.pyi | 1 - src-python/apngasm_python/apngasm.pyi | 45 -------------------------- 7 files changed, 1 insertion(+), 153 deletions(-) delete mode 100644 .github/workflows/update_stub.yml delete mode 100755 scripts/patch_stub.py delete mode 100755 scripts/update_stub.py delete mode 100644 src-python/apngasm_python/__init__.pyi delete mode 100644 src-python/apngasm_python/apngasm.pyi diff --git a/.github/workflows/update_stub.yml b/.github/workflows/update_stub.yml deleted file mode 100644 index 43ec3f5..0000000 --- a/.github/workflows/update_stub.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Update stub - -on: - push: - -jobs: - stubgen: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - with: - fetch-depth: 0 # otherwise, you will failed to push refs to dest repo - submodules: recursive - - name: Extract branch name - shell: bash - run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT - id: extract_branch - - name: Auto stub generation - run: | - pip install build - python scripts/update_stub.py - - name: Commit & Push changes - uses: actions-js/push@master - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - message: Auto stub generation - branch: ${{ steps.extract_branch.outputs.branch }} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index cc82b33..48f42c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -114,23 +114,10 @@ install(TARGETS _apngasm_python COMPONENT python_modules) # Generate stubs for the Python module -# option(WITH_PY_STUBS -# "Generate Python stub files (.pyi) for the Python module." On) -# if (WITH_PY_STUBS AND NOT CMAKE_CROSSCOMPILING) -# include(cmake/NanobindStubgen.cmake) -# nanobind_stubgen(_apngasm_python) -# add_custom_command(TARGET _apngasm_python POST_BUILD -# COMMAND ${PYTHON_EXECUTABLE} patch_stub.py $/_apngasm_python.pyi -# WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/scripts -# USES_TERMINAL) -# nanobind_stubgen_install(_apngasm_python ${PY_BUILD_CMAKE_MODULE_NAME}) -# endif() - # nanobind_add_stub( # apngasm_python_stub -# MODULE apngasm_python +# MODULE apngasm_python._apngasm_python # OUTPUT src-python/apngasm_python/_apngasm_python.pyi -# DEPENDS _apngasm_python # MARKER_FILE src-python/apngasm_python/py.typed # VERBOSE # ) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 2636c48..d45f893 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,9 +68,6 @@ config = "Release" [tool.py-build-cmake.windows.cmake.options] CMAKE_CXX_FLAGS_RELWITHDEBINFO = "/Zi /Ob0 /Od /RTC1" -[tool.py-build-cmake.stubgen] -args = ["-v"] - [tool.cibuildwheel] build-verbosity = 1 environment = { PY_BUILD_CMAKE_VERBOSE="1" } \ No newline at end of file diff --git a/scripts/patch_stub.py b/scripts/patch_stub.py deleted file mode 100755 index be93867..0000000 --- a/scripts/patch_stub.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/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" - ) - f.write(init_pyi) - - -if __name__ == "__main__": - main() diff --git a/scripts/update_stub.py b/scripts/update_stub.py deleted file mode 100755 index ef249ff..0000000 --- a/scripts/update_stub.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python3 -import os -from pathlib import Path -import shutil -import zipfile - - -def main(): - py_bin = shutil.which("python3") - if not py_bin: - py_bin = shutil.which("python") - if not py_bin: - raise RuntimeError("Cannot find path for python") - - proj_dir = Path(Path(__file__).parent, "../").resolve() - dist_dir = Path(proj_dir, "dist") - - shutil.rmtree(dist_dir, ignore_errors=True) - os.chdir(proj_dir) - os.system(py_bin + " -m build .") - - for zip_file in os.listdir(dist_dir): - 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": - continue - 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: - f.write(archive.read(file)) - - -if __name__ == "__main__": - main() diff --git a/src-python/apngasm_python/__init__.pyi b/src-python/apngasm_python/__init__.pyi deleted file mode 100644 index bda5b5a..0000000 --- a/src-python/apngasm_python/__init__.pyi +++ /dev/null @@ -1 +0,0 @@ -__version__: str diff --git a/src-python/apngasm_python/apngasm.pyi b/src-python/apngasm_python/apngasm.pyi deleted file mode 100644 index 98fef66..0000000 --- a/src-python/apngasm_python/apngasm.pyi +++ /dev/null @@ -1,45 +0,0 @@ -import numpy.typing -from ._apngasm_python import APNGAsm as APNGAsm, APNGFrame as APNGFrame, IAPNGAsmListener as IAPNGAsmListener, __version__ as __version__, create_frame_from_rgb as create_frame_from_rgb, create_frame_from_rgb_trns as create_frame_from_rgb_trns, create_frame_from_rgba as create_frame_from_rgba -from PIL import Image -from _typeshed import Incomplete -from typing import Any, Optional - -NUMPY_LOADED: bool -PILLOW_LOADED: bool - -class APNGAsmBinder: - color_type_dict: Incomplete - apngasm: Incomplete - def __init__(self) -> None: ... - def __enter__(self): ... - def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: types.TracebackType | None) -> None: ... - def frame_pixels_as_pillow(self, frame: int, new_value: Optional[Image.Image] = None) -> Optional[Image.Image]: ... - def frame_pixels_as_numpy(self, frame: int, new_value: Optional[numpy.typing.NDArray[Any]] = None) -> Optional[numpy.typing.NDArray[Any]]: ... - def frame_width(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: ... - def frame_height(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: ... - def frame_color_type(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: ... - def frame_palette(self, frame: int, new_value: Optional[numpy.typing.NDArray[Any]] = None) -> Optional[numpy.typing.NDArray[Any]]: ... - def frame_transparency(self, frame: int, new_value: Optional[numpy.typing.NDArray[Any]] = None) -> Optional[numpy.typing.NDArray[Any]]: ... - def frame_palette_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]: ... - def frame_delay_num(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: ... - def frame_delay_den(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: ... - def add_frame_from_file(self, file_path: str, delay_num: int = 100, delay_den: int = 1000) -> int: ... - def add_frame_from_pillow(self, pillow_image: Image.Image, delay_num: int = 100, delay_den: int = 1000) -> int: ... - 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: ... - def assemble(self, output_path: str) -> bool: ... - def disassemble_as_numpy(self, file_path: str) -> list[APNGFrame]: ... - def disassemble_as_pillow(self, file_path: str) -> list[APNGFrame]: ... - def save_pngs(self, output_dir: str) -> bool: ... - def load_animation_spec(self, file_path: str) -> list[APNGFrame]: ... - def save_json(self, output_path: str, image_dir: str) -> bool: ... - def save_xml(self, output_path: str, image_dir: str) -> bool: ... - def set_apng_asm_listener(self, listener: Optional[IAPNGAsmListener] = None): ... - def set_loops(self, loops: int = 0): ... - def set_skip_first(self, skip_first: bool): ... - def get_frames(self) -> list[APNGFrame]: ... - def get_loops(self) -> int: ... - def is_skip_first(self) -> int: ... - def frame_count(self) -> int: ... - def reset(self) -> int: ... - def version(self) -> str: ... From 65f3fb9e00d0bab3508cb523c08ed43b1ab94ad3 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sun, 18 Feb 2024 02:33:08 +0800 Subject: [PATCH 004/103] WIP: Generate stub with nanobind --- CMakeLists.txt | 15 +-- cmake/NanobindStubgen.cmake | 23 ---- src-python/apngasm_python/__init__.py | 1 + .../{_apngasm_python.pyi => __init__.pyi} | 26 ++--- src-python/apngasm_python/apngasm.py | 104 ++++++++++-------- src/apngasm_python.cpp | 6 +- 6 files changed, 83 insertions(+), 92 deletions(-) delete mode 100644 cmake/NanobindStubgen.cmake rename src-python/apngasm_python/{_apngasm_python.pyi => __init__.pyi} (93%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 48f42c3..f5d346d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -114,10 +114,11 @@ install(TARGETS _apngasm_python COMPONENT python_modules) # Generate stubs for the Python module -# nanobind_add_stub( -# apngasm_python_stub -# MODULE apngasm_python._apngasm_python -# OUTPUT src-python/apngasm_python/_apngasm_python.pyi -# MARKER_FILE src-python/apngasm_python/py.typed -# VERBOSE -# ) \ No newline at end of file +nanobind_add_stub( + apngasm_python_stub + INSTALL_TIME + MODULE apngasm_python + OUTPUT __init__.pyi + MARKER_FILE py.typed + VERBOSE +) \ No newline at end of file diff --git a/cmake/NanobindStubgen.cmake b/cmake/NanobindStubgen.cmake deleted file mode 100644 index 9e49004..0000000 --- a/cmake/NanobindStubgen.cmake +++ /dev/null @@ -1,23 +0,0 @@ -function(nanobind_stubgen target) - - find_package(Python REQUIRED COMPONENTS Interpreter) - - add_custom_command(TARGET ${target} POST_BUILD - COMMAND ${Python_EXECUTABLE} -m nanobind_stubgen - --out $ - $ - WORKING_DIRECTORY $ - USES_TERMINAL) - -endfunction() - -function(nanobind_stubgen_install target destination) - - install(FILES - $/$.pyi - RENAME __init__.pyi - EXCLUDE_FROM_ALL - COMPONENT python_modules - DESTINATION ${destination}/$) - -endfunction() \ No newline at end of file diff --git a/src-python/apngasm_python/__init__.py b/src-python/apngasm_python/__init__.py index 56fca9e..fc95b24 100755 --- a/src-python/apngasm_python/__init__.py +++ b/src-python/apngasm_python/__init__.py @@ -1,3 +1,4 @@ #!/usr/bin/env python3 """apngasm-python""" __version__ = "1.2.3" +from . import _apngasm_python \ No newline at end of file diff --git a/src-python/apngasm_python/_apngasm_python.pyi b/src-python/apngasm_python/__init__.pyi similarity index 93% rename from src-python/apngasm_python/_apngasm_python.pyi rename to src-python/apngasm_python/__init__.pyi index bc1af79..a767ec4 100644 --- a/src-python/apngasm_python/_apngasm_python.pyi +++ b/src-python/apngasm_python/__init__.pyi @@ -11,14 +11,14 @@ class APNGAsm: """Construct an empty APNGAsm object.""" @overload - def __init__(self, frames: Sequence[apngasm_python._apngasm_python.APNGFrame]) -> None: + def __init__(self, frames: Sequence[apngasm_python.APNGFrame]) -> None: """ Construct APNGAsm object from an existing vector of apngasm frames. :param list[apngasm_python._apngasm_python.APNGFrame] frames: A list of APNGFrame objects. """ - def add_frame(self, frame: apngasm_python._apngasm_python.APNGFrame) -> int: + def add_frame(self, frame: apngasm_python.APNGFrame) -> int: """ Adds an APNGFrame object to the frame vector. @@ -41,7 +41,7 @@ class APNGAsm: :rtype: int """ - def add_frame_from_rgb(self, pixels_rgb: apngasm_python._apngasm_python.rgb, width: int, height: int, trns_color: apngasm_python._apngasm_python.rgb = 0, delay_num: int = 100, delay_den: int = 1000) -> int: + def add_frame_from_rgb(self, pixels_rgb: apngasm_python.rgb, width: int, height: int, trns_color: Optional[apngasm_python.rgb] = 0, delay_num: int = 100, delay_den: int = 1000) -> int: """ Adds an APNGFrame object to the vector. Not possible to use in Python. As alternative, @@ -61,7 +61,7 @@ class APNGAsm: :rtype: int """ - def add_frame_from_rgba(self, pixels_rgba: apngasm_python._apngasm_python.rgba, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> int: + def add_frame_from_rgba(self, pixels_rgba: apngasm_python.rgba, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> int: """ Adds an APNGFrame object to the vector. Not possible to use in Python. As alternative, @@ -90,7 +90,7 @@ class APNGAsm: :rtype: bool """ - def disassemble(self, file_path: str) -> list[apngasm_python._apngasm_python.APNGFrame]: + def disassemble(self, file_path: str) -> list[apngasm_python.APNGFrame]: """ Disassembles an APNG file. @@ -110,7 +110,7 @@ class APNGAsm: :rtype: bool """ - def load_animation_spec(self, file_path: str) -> list[apngasm_python._apngasm_python.APNGFrame]: + def load_animation_spec(self, file_path: str) -> list[apngasm_python.APNGFrame]: """ Loads an animation spec from JSON or XML. Loaded frames are added to the end of the frame vector. @@ -145,7 +145,7 @@ class APNGAsm: :rtype: bool """ - def set_apngasm_listener(self, listener: Optional[apngasm_python._apngasm_python.IAPNGAsmListener] = None) -> None: + def set_apngasm_listener(self, listener: Optional[apngasm_python.IAPNGAsmListener] = None) -> None: """ Sets a listener. @@ -166,7 +166,7 @@ class APNGAsm: :param int skip_first: Flag of skip first frame. """ - def get_frames(self) -> list[apngasm_python._apngasm_python.APNGFrame]: + def get_frames(self) -> list[apngasm_python.APNGFrame]: """ Returns the frame vector. @@ -233,7 +233,7 @@ class APNGFrame: """ @overload - def __init__(self, pixels: apngasm_python._apngasm_python.rgb, width: int, height: int, trns_color: apngasm_python._apngasm_python.rgb, delay_num: int = 100, delay_den: int = 1000) -> None: + def __init__(self, pixels: apngasm_python.rgb, width: int, height: int, trns_color: apngasm_python.rgb, delay_num: int = 100, delay_den: int = 1000) -> None: """ Creates an APNGFrame from a bitmapped array of RBG pixel data. Not possible to use in Python. To create APNGFrame from pixel data in memory, @@ -251,7 +251,7 @@ class APNGFrame: """ @overload - def __init__(self, pixels: apngasm_python._apngasm_python.rgba, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> None: + def __init__(self, pixels: apngasm_python.rgba, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> None: """ Creates an APNGFrame from a bitmapped array of RBGA pixel data. Not possible to use in Python. To create APNGFrame from pixel data in memory, @@ -371,7 +371,7 @@ class APNGFrame: class IAPNGAsmListener: """Class for APNGAsmListener. Meant to be used internally.""" -def create_frame_from_rgb(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 3))], width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> apngasm_python._apngasm_python.APNGFrame: +def create_frame_from_rgb(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 3))], width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> apngasm_python.APNGFrame: """ Creates an APNGFrame from a bitmapped array of RBG pixel data. @@ -385,7 +385,7 @@ def create_frame_from_rgb(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape :rtype: apngasm_python._apngasm_python.APNGFrame """ -def create_frame_from_rgb_trns(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 3))], width: int, height: int, trns_color: Annotated[ArrayLike, dict(dtype='uint8', shape=(3))], delay_num: int = 100, delay_den: int = 1000) -> apngasm_python._apngasm_python.APNGFrame: +def create_frame_from_rgb_trns(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 3))], width: int, height: int, trns_color: Annotated[ArrayLike, dict(dtype='uint8', shape=(3))], delay_num: int = 100, delay_den: int = 1000) -> apngasm_python.APNGFrame: """ Creates an APNGFrame from a bitmapped array of RBG pixel data, with one color treated as transparent. @@ -400,7 +400,7 @@ def create_frame_from_rgb_trns(pixels: Annotated[ArrayLike, dict(dtype='uint8', :rtype: apngasm_python._apngasm_python.APNGFrame """ -def create_frame_from_rgba(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 4))], width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> apngasm_python._apngasm_python.APNGFrame: +def create_frame_from_rgba(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 4))], width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> apngasm_python.APNGFrame: """ Creates an APNGFrame from a bitmapped array of RBGA pixel data. diff --git a/src-python/apngasm_python/apngasm.py b/src-python/apngasm_python/apngasm.py index 73c7f67..1aa6ec2 100755 --- a/src-python/apngasm_python/apngasm.py +++ b/src-python/apngasm_python/apngasm.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 from __future__ import annotations -from ._apngasm_python import ( +from . import ( APNGAsm, APNGFrame, IAPNGAsmListener, create_frame_from_rgb, create_frame_from_rgb_trns, create_frame_from_rgba, + __version__, # type: ignore ) -from ._apngasm_python import __version__ # type: ignore try: import numpy @@ -16,14 +16,14 @@ NUMPY_LOADED = True except ModuleNotFoundError: - NUMPY_LOADED = False + NUMPY_LOADED = False # type: ignore try: from PIL import Image PILLOW_LOADED = True except ModuleNotFoundError: - PILLOW_LOADED = False + PILLOW_LOADED = False # type: ignore from typing import Optional, Any @@ -45,7 +45,7 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): # type: ignore self.apngasm.reset() - if PILLOW_LOADED: + if PILLOW_LOADED and NUMPY_LOADED: def frame_pixels_as_pillow( self, frame: int, new_value: Optional[Image.Image] = None @@ -62,10 +62,10 @@ def frame_pixels_as_pillow( :rtype: Optional[PIL.Image.Image] """ if new_value: - self.apngasm.get_frames()[frame].pixels = numpy.array(new_value) + self.apngasm.get_frames()[frame].pixels = numpy.array(new_value) # type: ignore else: mode = self.color_type_dict[self.apngasm.get_frames()[frame].color_type] - return Image.frombytes( + return Image.frombytes( # type: ignore mode, ( self.apngasm.get_frames()[frame].width, @@ -93,7 +93,7 @@ def frame_pixels_as_numpy( if new_value: self.apngasm.get_frames()[frame].pixels = new_value else: - return self.apngasm.get_frames()[frame].pixels + return numpy.array(self.apngasm.get_frames()[frame].pixels) # type: ignore def frame_width(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: """ @@ -172,7 +172,7 @@ def frame_palette( if new_value: self.apngasm.get_frames()[frame].palette = new_value else: - return self.apngasm.get_frames()[frame].palette + return numpy.array(self.apngasm.get_frames()[frame].palette) # type: ignore def frame_transparency( self, frame: int, new_value: Optional[numpy.typing.NDArray[Any]] = None @@ -192,7 +192,7 @@ def frame_transparency( if new_value: self.apngasm.get_frames()[frame].transparency = new_value else: - return self.apngasm.get_frames()[frame].transparency + return numpy.array(self.apngasm.get_frames()[frame].transparency) # type: ignore def frame_palette_size( self, frame: int, new_value: Optional[int] = None @@ -285,7 +285,7 @@ def add_frame_from_file( file_path=file_path, delay_num=delay_num, delay_den=delay_den ) - if PILLOW_LOADED: + if PILLOW_LOADED and NUMPY_LOADED: def add_frame_from_pillow( self, pillow_image: Image.Image, delay_num: int = 100, delay_den: int = 1000 @@ -304,8 +304,8 @@ def add_frame_from_pillow( """ 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), + return self.add_frame_from_numpy( # type: ignore + numpy_data=numpy.array(pillow_image), # type: ignore width=pillow_image.width, height=pillow_image.height, mode=pillow_image.mode, @@ -317,10 +317,10 @@ def add_frame_from_pillow( def add_frame_from_numpy( self, - numpy_data: numpy.typing.NDArray, + numpy_data: numpy.typing.NDArray[Any], width: Optional[int] = None, height: Optional[int] = None, - trns_color: Optional[numpy.typing.NDArray] = None, + trns_color: Optional[numpy.typing.NDArray[Any]] = None, mode: Optional[str] = None, delay_num: int = 100, delay_den: int = 1000, @@ -345,23 +345,24 @@ def add_frame_from_numpy( :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] + width = width if width else numpy.shape(numpy_data)[1] # type: ignore + height = height if height else numpy.shape(numpy_data)[0] # type: ignore if not mode: - if numpy.shape(numpy_data)[2] == 3: + if numpy.shape(numpy_data)[2] == 3: # type: ignore mode = "RGB" - elif numpy.shape(numpy_data)[2] == 4: + elif numpy.shape(numpy_data)[2] == 4: # type: ignore 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]}." + "The given numpy_data 3rd dimension size was " + f"{numpy.shape(numpy_data)[2]}." # type: ignore ) if mode == "RGB": - if type(trns_color) == numpy.typing.NDArray: + if type(trns_color) == numpy.typing.NDArray: # type: ignore frame = create_frame_from_rgb_trns( pixels=numpy_data, width=width, @@ -379,7 +380,7 @@ def add_frame_from_numpy( delay_den=delay_den, ) elif mode == "RGBA": - if type(trns_color) == numpy.typing.NDArray: + if type(trns_color) == numpy.typing.NDArray: # type: ignore raise TypeError( "Cannot set trns_color on RGBA mode Pillow object. Must be RGB." ) @@ -406,36 +407,45 @@ def assemble(self, output_path: str) -> 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. + if NUMPY_LOADED: - :param str file_path: The file path to the PNG image to be disassembled. + def disassemble_as_numpy(self, file_path: str) -> list[numpy.typing.NDArray[Any]]: + """ + Disassembles an APNG file to a list of frames, expressed as 3D numpy array. - :return: A list containing the frames of the disassembled PNG. - :rtype: list[apngasm_python._apngasm_python.APNGFrame] - """ - return self.apngasm.disassemble(file_path) + :param str file_path: The file path to the PNG image to be disassembled. - def disassemble_as_pillow(self, file_path: str) -> list[APNGFrame]: - """ - Disassembles an APNG file to a list of frames, expressed as Pillow images. + :return: A list containing the frames of the disassembled PNG. + :rtype: list[apngasm_python._apngasm_python.APNGFrame] + """ + frames = self.apngasm.disassemble(file_path) + frames_numpy: list[numpy.typing.NDArray[Any]] = [] + for frame in frames: + frames_numpy.append(numpy.array(frame.pixels)) # type: ignore - :param str file_path: The file path to the PNG image to be disassembled. + return frames_numpy - :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 - ) - frames_pillow.append(frame_pillow) + if PILLOW_LOADED: + + def disassemble_as_pillow(self, file_path: str) -> list[Image.Image]: + """ + 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[Image.Image] + """ + frames = self.apngasm.disassemble(file_path) + frames_pillow: list[Image.Image] = [] + for frame in frames: + mode = self.color_type_dict[frame.color_type] + frame_pillow = Image.frombytes( # type: ignore + mode, (frame.width, frame.height), frame.pixels + ) + frames_pillow.append(frame_pillow) - return frames_pillow + return frames_pillow def save_pngs(self, output_dir: str) -> bool: """ @@ -490,7 +500,7 @@ def save_xml(self, output_path: str, image_dir: str) -> bool: """ return self.apngasm.save_xml(output_path, image_dir) - def set_apng_asm_listener(self, listener: Optional[IAPNGAsmListener] = None): + def set_apng_asm_listener(self, listener: Optional[IAPNGAsmListener] = None): # type: ignore """ Sets a listener. You probably won't need to use this function. diff --git a/src/apngasm_python.cpp b/src/apngasm_python.cpp index aaca366..4765cb4 100644 --- a/src/apngasm_python.cpp +++ b/src/apngasm_python.cpp @@ -32,7 +32,9 @@ std::map rowbytesMap = { { 6, 4 } // RGBA }; -NB_MODULE(MODULE_NAME, m) { +NB_MODULE(MODULE_NAME, m_) { + (void) m_; /* unused */ + nb::module_ m = nb::module_::import_("apngasm_python"); m.doc() = "A nanobind API for apngasm, a tool/library for APNG assembly/disassembly"; m.attr("__version__") = VERSION_INFO; @@ -423,7 +425,7 @@ NB_MODULE(MODULE_NAME, m) { )pbdoc") .def("add_frame_from_rgb", nb::overload_cast(&apngasm::APNGAsm::addFrame), - "pixels_rgb"_a, "width"_a, "height"_a, "trns_color"_a = NULL, "delay_num"_a = apngasm::DEFAULT_FRAME_NUMERATOR, "delay_den"_a = apngasm::DEFAULT_FRAME_DENOMINATOR, + "pixels_rgb"_a, "width"_a, "height"_a, "trns_color"_a.none() = NULL, "delay_num"_a = apngasm::DEFAULT_FRAME_NUMERATOR, "delay_den"_a = apngasm::DEFAULT_FRAME_DENOMINATOR, R"pbdoc( Adds an APNGFrame object to the vector. Not possible to use in Python. As alternative, From 3903c20aaad2b2daf147e26ee685c9bd615e53bd Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sun, 18 Feb 2024 03:44:38 +0800 Subject: [PATCH 005/103] WIP: Generate stub with nanobind --- .gitignore | 3 +- README.md | 29 +- docs/conf.py | 4 +- example/example_direct.py | 8 +- src-python/apngasm_python/__init__.py | 2 +- src-python/apngasm_python/apngasm.py | 455 +++++++++++++------------- 6 files changed, 251 insertions(+), 250 deletions(-) diff --git a/.gitignore b/.gitignore index 26cf5ec..958e8c3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ dist/ .vscode/ conan_output/ example/output/ -venv/ \ No newline at end of file +venv/ +docs/_build/ \ No newline at end of file diff --git a/README.md b/README.md index f6fc37d..e8e728b 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,22 @@ # apngasm-python -A nanobind API for [apngasm](https://github.com/apngasm/apngasm), a tool/library for APNG assembly/disassembly. +A nanobind API for [apngasm](https://github.com/apngasm/apngasm), a tool/library for +APNG assembly/disassembly. -apngasm is originally a CLI program for quickly assembling PNG images into animated PNG (APNG). It also supports creating compressed APNG. +apngasm is originally a CLI program for quickly assembling PNG images into +animated PNG (APNG). It also supports creating compressed APNG. -apngasm-python is a binding for apngasm using nanobind, allowing you to use apngasm without calling it using commands. +apngasm-python is a binding for apngasm using nanobind, allowing you to use apngasm +without calling it using commands. -With this module, you can even create APNG using images inside memory (No need to write them out as file and call apngasm! This is about 2 times faster from testing.) +With this module, you can even create APNG using images inside memory (No need to write +them out as file and call apngasm! This is about 2 times faster from testing.) -A similar python module is https://github.com/eight04/pyAPNG , which handles APNG files with python natively and does not support compression. +A similar python module is https://github.com/eight04/pyAPNG , which handles APNG files +with python natively and does not support compression. -For convenience, prebuilt library is packaged with this module, so you need not download apngasm. +For convenience, prebuilt library is packaged with this module, so you need not +download apngasm. Documentations: https://apngasm-python.readthedocs.io/en/latest/ @@ -25,7 +31,8 @@ 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) +The recommended usage is to `from apngasm_python.apngasm import APNGAsmBinder`, see +[example/example_binder.py](example/example_binder.py) ```python from apngasm_python.apngasm import APNGAsmBinder import numpy as np @@ -64,9 +71,10 @@ with APNGAsmBinder() as apng: apngasm.save_pngs('output') ``` -Alternatively, you can reduce overhead and do advanced tasks by calling methods directly, see [example/example_direct.py](example/example_direct.py) +Alternatively, you can reduce overhead and do advanced tasks by calling methods +directly, see [example/example_direct.py](example/example_direct.py) ```python -from apngasm_python._apngasm_python import APNGAsm, APNGFrame, create_frame_from_rgb, create_frame_from_rgba +from apngasm_python import APNGAsm, APNGFrame, create_frame_from_rgb, create_frame_from_rgba import numpy as np from PIL import Image import os @@ -105,7 +113,8 @@ im.save('output/ball0.png') apngasm.save_pngs('output') ``` -The methods are based on [apngasm.h](https://github.com/apngasm/apngasm/blob/master/lib/src/apngasm.h) and [apngframe.h](https://github.com/apngasm/apngasm/blob/master/lib/src/apngframe.h) +The methods are based on [apngasm.h](https://github.com/apngasm/apngasm/blob/master/lib/src/apngasm.h) +and [apngframe.h](https://github.com/apngasm/apngasm/blob/master/lib/src/apngframe.h) You can get more info about the binding from [src/apngasm_python.cpp](src/apngasm_python.cpp), or by... diff --git a/docs/conf.py b/docs/conf.py index 4609efd..a424a8b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -33,9 +33,9 @@ autoapi_dirs = [ os.path.abspath("../src-python/apngasm_python"), - os.path.abspath("../src-python/apngasm_python/_apngasm_python"), ] autoapi_python_use_implicit_namespaces = True +autoapi_file_patterns = ["*.pyi", "*.py"] # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output @@ -44,7 +44,7 @@ html_static_path = ["_static"] # material theme options (see theme.conf for more information) html_theme_options = { - "repo_name": "rlottie-python", + "repo_name": "apngasm-python", "globaltoc_collapse": True, "features": [ "navigation.expand", diff --git a/example/example_direct.py b/example/example_direct.py index 7618e6d..56b86a2 100755 --- a/example/example_direct.py +++ b/example/example_direct.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -from apngasm_python._apngasm_python import ( +from apngasm_python import ( APNGAsm, APNGFrame, create_frame_from_rgb, @@ -12,7 +12,7 @@ import numpy as np -def frame_info(frame): +def frame_info(frame: APNGFrame): print(f"{frame.pixels = }") print(f"{frame.width = }") print(f"{frame.height = }") @@ -53,7 +53,7 @@ def frame_info(frame): # Getting one frame as Pillow Image mode = color_type_dict[frame.color_type] -im = Image.frombytes(mode, (frame.width, frame.height), frame.pixels) +im = Image.frombytes(mode, (frame.width, frame.height), frame.pixels) # type: ignore im.save("output/elephant-frame-pillow.png") # Get inforamtion about whole animation @@ -74,7 +74,7 @@ def frame_info(frame): frame = frames[0] frame_info(frame) mode = color_type_dict[frame.color_type] -im = Image.frombytes(mode, (frame.width, frame.height), frame.pixels) +im = Image.frombytes(mode, (frame.width, frame.height), frame.pixels) # type: ignore im.save("output/ball0.png") # Disassemble all APNG into PNGs diff --git a/src-python/apngasm_python/__init__.py b/src-python/apngasm_python/__init__.py index fc95b24..501096e 100755 --- a/src-python/apngasm_python/__init__.py +++ b/src-python/apngasm_python/__init__.py @@ -1,4 +1,4 @@ #!/usr/bin/env python3 """apngasm-python""" __version__ = "1.2.3" -from . import _apngasm_python \ No newline at end of file +from . import _apngasm_python # type: ignore \ No newline at end of file diff --git a/src-python/apngasm_python/apngasm.py b/src-python/apngasm_python/apngasm.py index 1aa6ec2..5680f85 100755 --- a/src-python/apngasm_python/apngasm.py +++ b/src-python/apngasm_python/apngasm.py @@ -1,5 +1,11 @@ #!/usr/bin/env python3 from __future__ import annotations +from typing import Optional, Any, TYPE_CHECKING + +if TYPE_CHECKING: + from numpy.typing import NDArray + from PIL import Image + from . import ( APNGAsm, APNGFrame, @@ -7,27 +13,8 @@ create_frame_from_rgb, create_frame_from_rgb_trns, create_frame_from_rgba, - __version__, # type: ignore ) -try: - import numpy - import numpy.typing - - NUMPY_LOADED = True -except ModuleNotFoundError: - NUMPY_LOADED = False # type: ignore - -try: - from PIL import Image - - PILLOW_LOADED = True -except ModuleNotFoundError: - PILLOW_LOADED = False # type: ignore - -from typing import Optional, Any - - class APNGAsmBinder: """ Python class for binding apngasm library @@ -45,55 +32,56 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): # type: ignore self.apngasm.reset() - if PILLOW_LOADED and NUMPY_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. - 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. - :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] + """ + from numpy import array + from PIL import Image - :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) # type: ignore - else: - mode = self.color_type_dict[self.apngasm.get_frames()[frame].color_type] - return Image.frombytes( # type: ignore - mode, - ( - self.apngasm.get_frames()[frame].width, - self.apngasm.get_frames()[frame].height, - ), - self.apngasm.get_frames()[frame].pixels, - ) + if new_value: + self.apngasm.get_frames()[frame].pixels = array(new_value) + else: + mode = self.color_type_dict[self.apngasm.get_frames()[frame].color_type] + return Image.frombytes( # type: ignore + 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[NDArray[Any]] = None + ) -> Optional[NDArray[Any]]: + """ + 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. - def frame_pixels_as_numpy( - self, frame: int, new_value: Optional[numpy.typing.NDArray[Any]] = None - ) -> Optional[numpy.typing.NDArray[Any]]: - """ - 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[NDArray] new_value: If set, then the raw pixel data of frame + is set with this value. - :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[NDArray] + """ + from numpy import array - :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 numpy.array(self.apngasm.get_frames()[frame].pixels) # type: ignore + if new_value: + self.apngasm.get_frames()[frame].pixels = new_value + else: + return array(self.apngasm.get_frames()[frame].pixels) def frame_width(self, frame: int, new_value: Optional[int] = None) -> Optional[int]: """ @@ -153,46 +141,48 @@ def frame_color_type( else: return self.apngasm.get_frames()[frame].color_type - if NUMPY_LOADED: + def frame_palette( + self, frame: int, new_value: Optional[NDArray[Any]] = None + ) -> Optional[NDArray[Any]]: + """ + 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]] - def frame_palette( - self, frame: int, new_value: Optional[numpy.typing.NDArray[Any]] = None - ) -> Optional[numpy.typing.NDArray[Any]]: - """ - 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[NDArray] new_value: If set, then the palette data of frame + is set with this value. - :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[NDArray] + """ + from numpy import array - :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 numpy.array(self.apngasm.get_frames()[frame].palette) # type: ignore - - def frame_transparency( - self, frame: int, new_value: Optional[numpy.typing.NDArray[Any]] = None - ) -> Optional[numpy.typing.NDArray[Any]]: - """ - 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 numpy.array(self.apngasm.get_frames()[frame].transparency) # type: ignore + if new_value: + self.apngasm.get_frames()[frame].palette = new_value + else: + return array(self.apngasm.get_frames()[frame].palette) + + def frame_transparency( + self, frame: int, new_value: Optional[NDArray[Any]] = None + ) -> Optional[NDArray[Any]]: + """ + 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[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[NDArray] + """ + from numpy import array + + if new_value: + self.apngasm.get_frames()[frame].transparency = new_value + else: + return array(self.apngasm.get_frames()[frame].transparency) def frame_palette_size( self, frame: int, new_value: Optional[int] = None @@ -285,116 +275,117 @@ def add_frame_from_file( file_path=file_path, delay_num=delay_num, delay_den=delay_den ) - if PILLOW_LOADED and NUMPY_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( # type: ignore - numpy_data=numpy.array(pillow_image), # type: ignore - width=pillow_image.width, - height=pillow_image.height, - mode=pillow_image.mode, - 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 + """ + from numpy import array + + if pillow_image.mode not in ("RGB", "RGBA"): + pillow_image = pillow_image.convert("RGBA") + return self.add_frame_from_numpy( + numpy_data=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: NDArray[Any], + width: Optional[int] = None, + height: Optional[int] = None, + trns_color: Optional[NDArray[Any]] = 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 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[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 + """ + from numpy import shape + from numpy.typing import NDArray - if NUMPY_LOADED: - - def add_frame_from_numpy( - self, - numpy_data: numpy.typing.NDArray[Any], - width: Optional[int] = None, - height: Optional[int] = None, - trns_color: Optional[numpy.typing.NDArray[Any]] = 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] # type: ignore - height = height if height else numpy.shape(numpy_data)[0] # type: ignore - - if not mode: - if numpy.shape(numpy_data)[2] == 3: # type: ignore - mode = "RGB" - elif numpy.shape(numpy_data)[2] == 4: # type: ignore - mode = "RGBA" - else: - raise TypeError( - "Cannot determine mode from numpy_data. " - "expected 3rd dimension size to be 3 (RGB) or 4 (RGBA). " - "The given numpy_data 3rd dimension size was " - f"{numpy.shape(numpy_data)[2]}." # type: ignore - ) - - if mode == "RGB": - if type(trns_color) == numpy.typing.NDArray: # type: ignore - 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: # type: ignore - raise TypeError( - "Cannot set trns_color on RGBA mode Pillow object. Must be RGB." - ) - frame = create_frame_from_rgba( + width = width if width else shape(numpy_data)[1] + height = height if height else shape(numpy_data)[0] + + if not mode: + if shape(numpy_data)[2] == 3: + mode = "RGB" + elif 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). " + "The given numpy_data 3rd dimension size was " + f"{shape(numpy_data)[2]}." + ) + + if mode == "RGB": + if type(trns_color) == 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: - raise TypeError(f"Invalid mode: {mode}. Must be RGB or RGBA.") + 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) == 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.") - return self.apngasm.add_frame(frame) + return self.apngasm.add_frame(frame) def assemble(self, output_path: str) -> bool: """ @@ -407,45 +398,45 @@ def assemble(self, output_path: str) -> bool: """ return self.apngasm.assemble(output_path) - if NUMPY_LOADED: + def disassemble_as_numpy(self, file_path: str) -> list[NDArray[Any]]: + """ + Disassembles an APNG file to a list of frames, expressed as 3D numpy array. - def disassemble_as_numpy(self, file_path: str) -> list[numpy.typing.NDArray[Any]]: - """ - 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. - :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.APNGFrame] + """ + from numpy import array - :return: A list containing the frames of the disassembled PNG. - :rtype: list[apngasm_python._apngasm_python.APNGFrame] - """ - frames = self.apngasm.disassemble(file_path) - frames_numpy: list[numpy.typing.NDArray[Any]] = [] - for frame in frames: - frames_numpy.append(numpy.array(frame.pixels)) # type: ignore + frames = self.apngasm.disassemble(file_path) + frames_numpy: list[NDArray[Any]] = [] + for frame in frames: + frames_numpy.append(array(frame.pixels)) - return frames_numpy + return frames_numpy - if PILLOW_LOADED: + def disassemble_as_pillow(self, file_path: str) -> list[Image.Image]: + """ + Disassembles an APNG file to a list of frames, expressed as Pillow images. - def disassemble_as_pillow(self, file_path: str) -> list[Image.Image]: - """ - 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. - :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[Image.Image] + """ + from PIL import Image - :return: A list containing the frames of the disassembled PNG. - :rtype: list[Image.Image] - """ - frames = self.apngasm.disassemble(file_path) - frames_pillow: list[Image.Image] = [] - for frame in frames: - mode = self.color_type_dict[frame.color_type] - frame_pillow = Image.frombytes( # type: ignore - mode, (frame.width, frame.height), frame.pixels - ) - frames_pillow.append(frame_pillow) + frames = self.apngasm.disassemble(file_path) + frames_pillow: list[Image.Image] = [] + for frame in frames: + mode = self.color_type_dict[frame.color_type] + frame_pillow = Image.frombytes( # type: ignore + mode, (frame.width, frame.height), frame.pixels + ) + frames_pillow.append(frame_pillow) - return frames_pillow + return frames_pillow def save_pngs(self, output_dir: str) -> bool: """ @@ -469,7 +460,7 @@ def load_animation_spec(self, file_path: str) -> list[APNGFrame]: :param str file_path: The path of JSON or XML file. :return: A vector containing the loaded frames. - :rtype: list[apngasm_python._apngasm_python.APNGFrame] + :rtype: list[apngasm_python.APNGFrame] """ return self.apngasm.load_animation_spec(file_path) @@ -505,7 +496,7 @@ 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. + :param Optional[apngasm_python.IAPNGAsmListener] listener: A pointer to the listener object. If the argument is None, a default APNGAsmListener will be created and assigned. """ @@ -533,7 +524,7 @@ def get_frames(self) -> list[APNGFrame]: Returns the frame vector. :return: frame vector. - :rtype: list[apngasm_python._apngasm_python.APNGFrame] + :rtype: list[apngasm_python.APNGFrame] """ return self.apngasm.get_frames() From ae66338292679a27b4bfaa2f457f01e72f730823 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sun, 18 Feb 2024 03:49:38 +0800 Subject: [PATCH 006/103] WIP: Generate stub with nanobind --- README.md | 4 ++-- src-python/apngasm_python/__init__.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e8e728b..f1f582f 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,10 @@ pip install Pillow numpy ``` ## Example usage -The recommended usage is to `from apngasm_python.apngasm import APNGAsmBinder`, see +The recommended usage is to `from apngasm_python import APNGAsmBinder`, see [example/example_binder.py](example/example_binder.py) ```python -from apngasm_python.apngasm import APNGAsmBinder +from apngasm_python import APNGAsmBinder import numpy as np from PIL import Image import os diff --git a/src-python/apngasm_python/__init__.py b/src-python/apngasm_python/__init__.py index 501096e..fe7eef3 100755 --- a/src-python/apngasm_python/__init__.py +++ b/src-python/apngasm_python/__init__.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 """apngasm-python""" __version__ = "1.2.3" -from . import _apngasm_python # type: ignore \ No newline at end of file +from . import _apngasm_python # type: ignore +from .apngasm import APNGAsmBinder # type: ignore \ No newline at end of file From c8d4f89bbe8a976a15d309f97cfad1e993043915 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sun, 18 Feb 2024 03:53:14 +0800 Subject: [PATCH 007/103] formatting with ruff --- src-python/apngasm_python/__init__.py | 4 ++-- src-python/apngasm_python/apngasm.py | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src-python/apngasm_python/__init__.py b/src-python/apngasm_python/__init__.py index fe7eef3..253702a 100755 --- a/src-python/apngasm_python/__init__.py +++ b/src-python/apngasm_python/__init__.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 """apngasm-python""" __version__ = "1.2.3" -from . import _apngasm_python # type: ignore -from .apngasm import APNGAsmBinder # type: ignore \ No newline at end of file +from . import _apngasm_python # type: ignore +from .apngasm import APNGAsmBinder # type: ignore diff --git a/src-python/apngasm_python/apngasm.py b/src-python/apngasm_python/apngasm.py index 5680f85..e5428a3 100755 --- a/src-python/apngasm_python/apngasm.py +++ b/src-python/apngasm_python/apngasm.py @@ -15,6 +15,7 @@ create_frame_from_rgba, ) + class APNGAsmBinder: """ Python class for binding apngasm library @@ -29,7 +30,7 @@ def __init__(self): def __enter__(self): return self - def __exit__(self, exc_type, exc_val, exc_tb): # type: ignore + def __exit__(self, exc_type, exc_val, exc_tb): # type: ignore self.apngasm.reset() def frame_pixels_as_pillow( @@ -53,7 +54,7 @@ def frame_pixels_as_pillow( self.apngasm.get_frames()[frame].pixels = array(new_value) else: mode = self.color_type_dict[self.apngasm.get_frames()[frame].color_type] - return Image.frombytes( # type: ignore + return Image.frombytes( # type: ignore mode, ( self.apngasm.get_frames()[frame].width, @@ -431,7 +432,7 @@ def disassemble_as_pillow(self, file_path: str) -> list[Image.Image]: frames_pillow: list[Image.Image] = [] for frame in frames: mode = self.color_type_dict[frame.color_type] - frame_pillow = Image.frombytes( # type: ignore + frame_pillow = Image.frombytes( # type: ignore mode, (frame.width, frame.height), frame.pixels ) frames_pillow.append(frame_pillow) @@ -491,7 +492,7 @@ def save_xml(self, output_path: str, image_dir: str) -> bool: """ return self.apngasm.save_xml(output_path, image_dir) - def set_apng_asm_listener(self, listener: Optional[IAPNGAsmListener] = None): # type: ignore + def set_apng_asm_listener(self, listener: Optional[IAPNGAsmListener] = None): # type: ignore """ Sets a listener. You probably won't need to use this function. From ca7ae854aa2fdb3da3dff891a5971525249586fe Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sun, 18 Feb 2024 09:31:50 +0800 Subject: [PATCH 008/103] WIP: Generate stub with nanobind --- CMakeLists.txt | 4 ++-- pyproject.toml | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f5d346d..98ad220 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -118,7 +118,7 @@ nanobind_add_stub( apngasm_python_stub INSTALL_TIME MODULE apngasm_python - OUTPUT __init__.pyi - MARKER_FILE py.typed + OUTPUT "${PY_BUILD_CMAKE_MODULE_NAME}/__init__.pyi" + MARKER_FILE "${PY_BUILD_CMAKE_MODULE_NAME}/py.typed" VERBOSE ) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d45f893..cd35106 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,6 @@ include = [ [tool.py-build-cmake.cmake] # How to build the CMake project build_type = "Release" source_path = "." -options = { "WITH_PY_STUBS:BOOL" = "On" } args = ["-Wdev"] find_python3 = false find_python = true From 11787ad3ff99b7565e88d5f6ed3f6451e8df7b1c Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sun, 18 Feb 2024 16:56:00 +0800 Subject: [PATCH 009/103] WIP: Generate stub with nanobind --- CMakeLists.txt | 1 + README.md | 6 +-- example/example_direct.py | 2 +- src-python/apngasm_python/__init__.py | 2 - .../{__init__.pyi => _apngasm_python.pyi} | 37 +++++++------------ src-python/apngasm_python/apngasm.py | 11 +++--- src/apngasm_python.cpp | 4 +- 7 files changed, 26 insertions(+), 37 deletions(-) rename src-python/apngasm_python/{__init__.pyi => _apngasm_python.pyi} (91%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 98ad220..468580d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -120,5 +120,6 @@ nanobind_add_stub( MODULE apngasm_python OUTPUT "${PY_BUILD_CMAKE_MODULE_NAME}/__init__.pyi" MARKER_FILE "${PY_BUILD_CMAKE_MODULE_NAME}/py.typed" + COMPONENT python_modules VERBOSE ) \ No newline at end of file diff --git a/README.md b/README.md index f1f582f..c774b9f 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,10 @@ pip install Pillow numpy ``` ## Example usage -The recommended usage is to `from apngasm_python import APNGAsmBinder`, see +The recommended usage is to `from apngasm_python.apngasm import APNGAsmBinder`, see [example/example_binder.py](example/example_binder.py) ```python -from apngasm_python import APNGAsmBinder +from apngasm_python.apngasm import APNGAsmBinder import numpy as np from PIL import Image import os @@ -74,7 +74,7 @@ apngasm.save_pngs('output') Alternatively, you can reduce overhead and do advanced tasks by calling methods directly, see [example/example_direct.py](example/example_direct.py) ```python -from apngasm_python import APNGAsm, APNGFrame, create_frame_from_rgb, create_frame_from_rgba +from apngasm_python._apngasm_python import APNGAsm, APNGFrame, create_frame_from_rgb, create_frame_from_rgba import numpy as np from PIL import Image import os diff --git a/example/example_direct.py b/example/example_direct.py index 56b86a2..2ce61ee 100755 --- a/example/example_direct.py +++ b/example/example_direct.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -from apngasm_python import ( +from apngasm_python._apngasm_python import ( APNGAsm, APNGFrame, create_frame_from_rgb, diff --git a/src-python/apngasm_python/__init__.py b/src-python/apngasm_python/__init__.py index 253702a..56fca9e 100755 --- a/src-python/apngasm_python/__init__.py +++ b/src-python/apngasm_python/__init__.py @@ -1,5 +1,3 @@ #!/usr/bin/env python3 """apngasm-python""" __version__ = "1.2.3" -from . import _apngasm_python # type: ignore -from .apngasm import APNGAsmBinder # type: ignore diff --git a/src-python/apngasm_python/__init__.pyi b/src-python/apngasm_python/_apngasm_python.pyi similarity index 91% rename from src-python/apngasm_python/__init__.pyi rename to src-python/apngasm_python/_apngasm_python.pyi index a767ec4..2814149 100644 --- a/src-python/apngasm_python/__init__.pyi +++ b/src-python/apngasm_python/_apngasm_python.pyi @@ -1,8 +1,5 @@ from numpy.typing import ArrayLike -from typing import Annotated -from typing import Optional -from typing import Sequence -from typing import overload +from typing import overload, Sequence, Annotated class APNGAsm: """Class representing APNG file, storing APNGFrame(s) and other metadata.""" @@ -11,14 +8,14 @@ class APNGAsm: """Construct an empty APNGAsm object.""" @overload - def __init__(self, frames: Sequence[apngasm_python.APNGFrame]) -> None: + def __init__(self, frames: Sequence[APNGFrame]) -> None: """ Construct APNGAsm object from an existing vector of apngasm frames. :param list[apngasm_python._apngasm_python.APNGFrame] frames: A list of APNGFrame objects. """ - def add_frame(self, frame: apngasm_python.APNGFrame) -> int: + def add_frame(self, frame: APNGFrame) -> int: """ Adds an APNGFrame object to the frame vector. @@ -41,7 +38,7 @@ class APNGAsm: :rtype: int """ - def add_frame_from_rgb(self, pixels_rgb: apngasm_python.rgb, width: int, height: int, trns_color: Optional[apngasm_python.rgb] = 0, delay_num: int = 100, delay_den: int = 1000) -> int: + def add_frame_from_rgb(self, pixels_rgb: rgb, width: int, height: int, trns_color: rgb | None = 0, delay_num: int = 100, delay_den: int = 1000) -> int: """ Adds an APNGFrame object to the vector. Not possible to use in Python. As alternative, @@ -61,7 +58,7 @@ class APNGAsm: :rtype: int """ - def add_frame_from_rgba(self, pixels_rgba: apngasm_python.rgba, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> int: + def add_frame_from_rgba(self, pixels_rgba: rgba, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> int: """ Adds an APNGFrame object to the vector. Not possible to use in Python. As alternative, @@ -90,7 +87,7 @@ class APNGAsm: :rtype: bool """ - def disassemble(self, file_path: str) -> list[apngasm_python.APNGFrame]: + def disassemble(self, file_path: str) -> list[APNGFrame]: """ Disassembles an APNG file. @@ -110,7 +107,7 @@ class APNGAsm: :rtype: bool """ - def load_animation_spec(self, file_path: str) -> list[apngasm_python.APNGFrame]: + 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. @@ -145,7 +142,7 @@ class APNGAsm: :rtype: bool """ - def set_apngasm_listener(self, listener: Optional[apngasm_python.IAPNGAsmListener] = None) -> None: + def set_apngasm_listener(self, listener: IAPNGAsmListener | None = None) -> None: """ Sets a listener. @@ -166,7 +163,7 @@ class APNGAsm: :param int skip_first: Flag of skip first frame. """ - def get_frames(self) -> list[apngasm_python.APNGFrame]: + def get_frames(self) -> list[APNGFrame]: """ Returns the frame vector. @@ -233,7 +230,7 @@ class APNGFrame: """ @overload - def __init__(self, pixels: apngasm_python.rgb, width: int, height: int, trns_color: apngasm_python.rgb, delay_num: int = 100, delay_den: int = 1000) -> None: + def __init__(self, pixels: rgb, width: int, height: int, trns_color: rgb, delay_num: int = 100, delay_den: int = 1000) -> None: """ Creates an APNGFrame from a bitmapped array of RBG pixel data. Not possible to use in Python. To create APNGFrame from pixel data in memory, @@ -251,7 +248,7 @@ class APNGFrame: """ @overload - def __init__(self, pixels: apngasm_python.rgba, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> None: + def __init__(self, pixels: rgba, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> None: """ Creates an APNGFrame from a bitmapped array of RBGA pixel data. Not possible to use in Python. To create APNGFrame from pixel data in memory, @@ -371,7 +368,7 @@ class APNGFrame: class IAPNGAsmListener: """Class for APNGAsmListener. Meant to be used internally.""" -def create_frame_from_rgb(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 3))], width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> apngasm_python.APNGFrame: +def create_frame_from_rgb(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 3))], width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> APNGFrame: """ Creates an APNGFrame from a bitmapped array of RBG pixel data. @@ -385,7 +382,7 @@ def create_frame_from_rgb(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape :rtype: apngasm_python._apngasm_python.APNGFrame """ -def create_frame_from_rgb_trns(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 3))], width: int, height: int, trns_color: Annotated[ArrayLike, dict(dtype='uint8', shape=(3))], delay_num: int = 100, delay_den: int = 1000) -> apngasm_python.APNGFrame: +def create_frame_from_rgb_trns(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 3))], width: int, height: int, trns_color: Annotated[ArrayLike, dict(dtype='uint8', shape=(3))], delay_num: int = 100, delay_den: int = 1000) -> APNGFrame: """ Creates an APNGFrame from a bitmapped array of RBG pixel data, with one color treated as transparent. @@ -400,7 +397,7 @@ def create_frame_from_rgb_trns(pixels: Annotated[ArrayLike, dict(dtype='uint8', :rtype: apngasm_python._apngasm_python.APNGFrame """ -def create_frame_from_rgba(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 4))], width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> apngasm_python.APNGFrame: +def create_frame_from_rgba(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 4))], width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> APNGFrame: """ Creates an APNGFrame from a bitmapped array of RBGA pixel data. @@ -475,9 +472,3 @@ class rgba: @a.setter def a(self, arg: int, /) -> None: ... - -del overload -del Sequence -del Optional -del Annotated -del ArrayLike \ No newline at end of file diff --git a/src-python/apngasm_python/apngasm.py b/src-python/apngasm_python/apngasm.py index e5428a3..94437cc 100755 --- a/src-python/apngasm_python/apngasm.py +++ b/src-python/apngasm_python/apngasm.py @@ -6,13 +6,14 @@ from numpy.typing import NDArray from PIL import Image -from . import ( +from ._apngasm_python import ( # type: ignore APNGAsm, APNGFrame, IAPNGAsmListener, create_frame_from_rgb, create_frame_from_rgb_trns, create_frame_from_rgba, + __version__, # type: ignore ) @@ -406,7 +407,7 @@ def disassemble_as_numpy(self, file_path: str) -> list[NDArray[Any]]: :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.APNGFrame] + :rtype: list[apngasm_python._apngasm_python.APNGFrame] """ from numpy import array @@ -461,7 +462,7 @@ def load_animation_spec(self, file_path: str) -> list[APNGFrame]: :param str file_path: The path of JSON or XML file. :return: A vector containing the loaded frames. - :rtype: list[apngasm_python.APNGFrame] + :rtype: list[apngasm_python._apngasm_python.APNGFrame] """ return self.apngasm.load_animation_spec(file_path) @@ -497,7 +498,7 @@ 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.IAPNGAsmListener] listener: A pointer to the listener object. + :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. """ @@ -525,7 +526,7 @@ def get_frames(self) -> list[APNGFrame]: Returns the frame vector. :return: frame vector. - :rtype: list[apngasm_python.APNGFrame] + :rtype: list[apngasm_python._apngasm_python.APNGFrame] """ return self.apngasm.get_frames() diff --git a/src/apngasm_python.cpp b/src/apngasm_python.cpp index 4765cb4..e07fcfa 100644 --- a/src/apngasm_python.cpp +++ b/src/apngasm_python.cpp @@ -32,9 +32,7 @@ std::map rowbytesMap = { { 6, 4 } // RGBA }; -NB_MODULE(MODULE_NAME, m_) { - (void) m_; /* unused */ - nb::module_ m = nb::module_::import_("apngasm_python"); +NB_MODULE(MODULE_NAME, m) { m.doc() = "A nanobind API for apngasm, a tool/library for APNG assembly/disassembly"; m.attr("__version__") = VERSION_INFO; From 0a3d5e0ae892f164e265ac6836c1cc3b41a300f9 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sun, 18 Feb 2024 17:34:41 +0800 Subject: [PATCH 010/103] Fix docstrings --- src-python/apngasm_python/apngasm.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src-python/apngasm_python/apngasm.py b/src-python/apngasm_python/apngasm.py index 94437cc..f95a934 100755 --- a/src-python/apngasm_python/apngasm.py +++ b/src-python/apngasm_python/apngasm.py @@ -72,11 +72,11 @@ def frame_pixels_as_numpy( This should be set AFTER you set the width, height and color_type. :param int frame: Target frame number. - :param Optional[NDArray] new_value: If set, then the raw pixel data of frame + :param Optional[numpy.typing.NDArray[Any]] 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[NDArray] + :rtype: Optional[numpy.typing.NDArray[Any]] """ from numpy import array @@ -151,11 +151,11 @@ def frame_palette( 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[NDArray] new_value: If set, then the palette data of frame + :param Optional[numpy.typing.NDArray[Any]] 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[NDArray] + :rtype: Optional[numpy.typing.NDArray[Any]] """ from numpy import array @@ -173,11 +173,11 @@ def frame_transparency( http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html :param int frame: Target frame number. - :param Optional[NDArray] new_value: If set, then the transparency of frame + :param Optional[numpy.typing.NDArray[Any]] 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[NDArray] + :rtype: Optional[numpy.typing.NDArray[Any]] """ from numpy import array @@ -320,14 +320,14 @@ def add_frame_from_numpy( The frame duration is equal to delay_num / delay_den seconds. Default frame duration is 100/1000 second, or 0.1 second. - :param NDArray numpy_data: The pixel data, expressed as 3D numpy array. + :param numpy.typing.NDArray[Any] 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[NDArray] trns_color: The color [r, g, b] to be treated as transparent, expressed as 1D numpy array. + :param Optional[numpy.typing.NDArray[Any]] 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). @@ -407,7 +407,7 @@ def disassemble_as_numpy(self, file_path: str) -> list[NDArray[Any]]: :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] + :rtype: list[numpy.typing.NDArray[Any]] """ from numpy import array @@ -425,7 +425,7 @@ def disassemble_as_pillow(self, file_path: str) -> list[Image.Image]: :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[Image.Image] + :rtype: list[PIL.Image.Image] """ from PIL import Image From 47e6d175741d664120cebc903d5282d70687959a Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sun, 18 Feb 2024 20:04:17 +0800 Subject: [PATCH 011/103] WIP: commit check and stub generation to repo --- .github/workflows/check.yml | 39 ++ CMakeLists.txt | 1 + .../py.typed => scripts/__init__.py | 0 scripts/get_arch.py | 2 +- scripts/get_deps.py | 4 - src-python/apngasm_python/_apngasm_python.pyi | 474 ------------------ src-python/apngasm_python/apngasm.py | 1 - 7 files changed, 41 insertions(+), 480 deletions(-) create mode 100644 .github/workflows/check.yml rename src-python/apngasm_python/py.typed => scripts/__init__.py (100%) delete mode 100644 src-python/apngasm_python/_apngasm_python.pyi diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..0f00107 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,39 @@ +name: Update stub + +on: + push: + +jobs: + stubgen: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + with: + fetch-depth: 0 # otherwise, you will failed to push refs to dest repo + submodules: recursive + - name: Extract branch name + shell: bash + run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT + id: extract_branch + - uses: actions/setup-python@v5 + with: + python-version: '3.8' + - name: Check + run: | + pip install build ruff + pip install git+https://github.com/wjakob/nanobind.git@stubgen + ruff check ./src-python + ruff check ./scripts + python -m build . + ruff format ./src-python + ruff format ./scripts + python -m nanobind.stubgen \ + -m apngasm_python \ + -o src-python/apngasm_python/_apngasm_python.pyi \ + -M src-python/apngasm_python/py.typed + - name: Commit & Push changes + uses: actions-js/push@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + message: Update stub and formatting + branch: ${{ steps.extract_branch.outputs.branch }} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 468580d..d21d9c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,5 +121,6 @@ nanobind_add_stub( OUTPUT "${PY_BUILD_CMAKE_MODULE_NAME}/__init__.pyi" MARKER_FILE "${PY_BUILD_CMAKE_MODULE_NAME}/py.typed" COMPONENT python_modules + EXCLUDE_FROM_ALL VERBOSE ) \ No newline at end of file diff --git a/src-python/apngasm_python/py.typed b/scripts/__init__.py similarity index 100% rename from src-python/apngasm_python/py.typed rename to scripts/__init__.py diff --git a/scripts/get_arch.py b/scripts/get_arch.py index 0121c7c..8c7a782 100755 --- a/scripts/get_arch.py +++ b/scripts/get_arch.py @@ -22,7 +22,7 @@ def get_arch(): arch = k break - if arch == None: + if arch is None: arch = platform.machine().lower() return arch diff --git a/scripts/get_deps.py b/scripts/get_deps.py index ff7b82f..5c740ac 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -1,13 +1,9 @@ #!/usr/bin/env python3 import platform import os -import sys import subprocess import shutil -SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -sys.path.append(os.path.dirname(SCRIPT_DIR)) - from scripts.get_arch import conan_archs, get_arch diff --git a/src-python/apngasm_python/_apngasm_python.pyi b/src-python/apngasm_python/_apngasm_python.pyi deleted file mode 100644 index 2814149..0000000 --- a/src-python/apngasm_python/_apngasm_python.pyi +++ /dev/null @@ -1,474 +0,0 @@ -from numpy.typing import ArrayLike -from typing import overload, Sequence, Annotated - -class APNGAsm: - """Class representing APNG file, storing APNGFrame(s) and other metadata.""" - @overload - def __init__(self) -> None: - """Construct an empty APNGAsm object.""" - - @overload - def __init__(self, frames: Sequence[APNGFrame]) -> None: - """ - Construct APNGAsm object from an existing vector of apngasm frames. - - :param list[apngasm_python._apngasm_python.APNGFrame] frames: A list of APNGFrame objects. - """ - - def add_frame(self, frame: APNGFrame) -> int: - """ - Adds an APNGFrame object to the frame vector. - - :param frame: The APNGFrame object to be added. - :type frame: apngasm_python._apngasm_python.APNGFrame - - :return: The new number of frames/the number of this frame on the frame vector. - :rtype: 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 DEFAULT_FRAME_NUMERATOR). - :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - - :return: The new number of frames/the number of this frame on the frame vector. - :rtype: int - """ - - def add_frame_from_rgb(self, pixels_rgb: rgb, width: int, height: int, trns_color: rgb | None = 0, delay_num: int = 100, delay_den: int = 1000) -> int: - """ - Adds an APNGFrame object to the vector. - Not possible to use in Python. As alternative, - Use create_frame_from_rgb() or create_frame_from_rgba(). Or manually, - First create an empty APNGFrame with frame = APNGFrame(), - then set frame.width, frame.height, frame.color_type, frame.pixels, - frame.palette, frame.delay_num, frame.delay_den manually. - - :param apngasm_python._apngasm_python.rgb pixels_rgb: The RGB pixel data. - :param int width: The width of the pixel data. - :param int height: The height of the pixel data. - :param apngasm_python._apngasm_python.rgb trns_color: The color [r, g, b] to be treated as transparent. - :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). - :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - - :return: The new number of frames/the number of this frame on the frame vector. - :rtype: int - """ - - def add_frame_from_rgba(self, pixels_rgba: rgba, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> int: - """ - Adds an APNGFrame object to the vector. - Not possible to use in Python. As alternative, - Use create_frame_from_rgb() or create_frame_from_rgba(). Or manually, - First create an empty APNGFrame with frame = APNGFrame(), - then set frame.width, frame.height, frame.color_type, frame.pixels, - frame.palette, frame.delay_num, frame.delay_den manually. - - :param apngasm_python._apngasm_python.rgba pixels_rgba: The RGBA pixel data. - :param int width: The width of the pixel data. - :param int height: The height of the pixel data. - :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). - :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - - :return: The new number of frames/the number of this frame on the frame vector. - :rtype: int - """ - - 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 - """ - - def disassemble(self, file_path: str) -> list[APNGFrame]: - """ - Disassembles an APNG file. - - :param str file_path: The file path to the PNG image to be disassembled. - - :return: A vector containing the frames of the disassembled PNG. - :rtype: list[apngasm_python._apngasm_python.APNGFrame] - """ - - 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 - """ - - 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: - https://github.com/Genshin/PhantomStandards - - :param str file_path: The path of JSON or XML file. - - :return: A vector containing the frames - :rtype: list[apngasm_python._apngasm_python.APNGFrame] - """ - - def save_json(self, output_path: str, image_dir: str) -> bool: - """ - Saves a JSON animation spec file. - - :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 - """ - - def save_xml(self, output_path: str, image_dir: str) -> bool: - """ - Saves an XML animation spec file. - - :param str file_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 - """ - - def set_apngasm_listener(self, listener: IAPNGAsmListener | None = None) -> None: - """ - Sets a listener. - - :param Optional[apngasm_python._apngasm_python.IAPNGAsmListener] listener: A pointer to the listener object. If the argument is NULL a default APNGAsmListener will be created and assigned. - """ - - def set_loops(self, loops: int = 0) -> None: - """ - Set loop count of animation. - - :param int loops: Loop count of animation. If the argument is 0 a loop count is infinity. - """ - - def set_skip_first(self, skip_first: bool) -> None: - """ - Set flag of skip first frame. - - :param int skip_first: Flag of skip first frame. - """ - - def get_frames(self) -> list[APNGFrame]: - """ - Returns the frame vector. - - :return: frame vector. - :rtype: list[apngasm_python._apngasm_python.APNGFrame] - """ - - def get_loops(self) -> int: - """ - Returns the loop count. - - :return: loop count. - :rtype: int - """ - - def is_skip_first(self) -> bool: - """ - Returns the flag of skip first frame. - - :return: flag of skip first frame. - :rtype: bool - """ - - def frame_count(self) -> int: - """ - Returns the number of frames. - - :return: number of frames. - :rtype: int - """ - - 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 - """ - - def version(self) -> str: - """ - Returns the version of APNGAsm. - - :return: the version of APNGAsm. - :rtype: str - """ - -class APNGFrame: - """Class representing a frame in APNG.""" - @overload - def __init__(self) -> None: - """Creates an empty APNGFrame.""" - - @overload - def __init__(self, file_path: str, delay_num: int = 100, delay_den: int = 1000) -> None: - """ - Creates an APNGFrame from a PNG file. - - :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 DEFAULT_FRAME_NUMERATOR). - :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - """ - - @overload - def __init__(self, pixels: rgb, width: int, height: int, trns_color: rgb, delay_num: int = 100, delay_den: int = 1000) -> None: - """ - Creates an APNGFrame from a bitmapped array of RBG pixel data. - Not possible to use in Python. To create APNGFrame from pixel data in memory, - Use create_frame_from_rgb() or create_frame_from_rgba(). Or manually, - First create an empty APNGFrame with frame = APNGFrame(), - then set frame.width, frame.height, frame.color_type, frame.pixels, - frame.palette, frame.delay_num, frame.delay_den manually. - - :param apngasm_python._apngasm_python.rgb pixels: The RGB pixel data. - :param int width: The width of the pixel data. - :param int height: The height of the pixel data. - :param apngasm_python._apngasm_python.rgb trns_color: The color [r, g, b] to be treated as transparent. - :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). - :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - """ - - @overload - def __init__(self, pixels: rgba, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> None: - """ - Creates an APNGFrame from a bitmapped array of RBGA pixel data. - Not possible to use in Python. To create APNGFrame from pixel data in memory, - Use create_frame_from_rgb() or create_frame_from_rgba(). Or manually, - First create an empty APNGFrame with frame = APNGFrame(), - then set frame.width, frame.height, frame.color_type, frame.pixels, - frame.palette, frame.delay_num, frame.delay_den manually. - - :param apngasm_python._apngasm_python.rgba pixels: The RGBA pixel data. - :param int width: The width of the pixel data. - :param int height: The height of the pixel data. - :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). - :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - """ - - def save(self, out_path: str) -> bool: - """ - Saves this frame as a single PNG file. - - :param str out_path: The relative or absolute path to save the image file to. - - :return: true if save was successful. - :rtype: bool - """ - - @property - def pixels(self) -> Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, None))]: - """ - The raw pixel data of frame, expressed as a 3D numpy array in Python. - Note that setting this value will also set the variable 'rows' internally. - This should be set AFTER you set the width, height and color_type. - """ - - @pixels.setter - def pixels(self, arg: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, None))], /) -> None: ... - - @property - def width(self) -> int: - """The width of frame.""" - - @width.setter - def width(self, arg: int, /) -> None: ... - - @property - def height(self) -> int: - """The height of frame.""" - - @height.setter - def height(self, arg: int, /) -> None: ... - - @property - def color_type(self) -> int: - """ - The color_type of the frame. - - 0: Grayscale (Pillow mode='L') - 2: RGB (Pillow mode='RGB') - 3: Palette (Pillow mode='P') - 4: Grayscale + Alpha (Pillow mode='LA') - 6: RGBA (Pillow mode='RGBA') - """ - - @color_type.setter - def color_type(self, arg: int, /) -> None: ... - - @property - def palette(self) -> Annotated[ArrayLike, dict(dtype='uint8', shape=(256, 3))]: - """ - 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]] in Python. - """ - - @palette.setter - def palette(self, arg: Annotated[ArrayLike, dict(dtype='uint8', shape=(256, 3))], /) -> None: ... - - @property - def transparency(self) -> Annotated[ArrayLike, dict(dtype='uint8', shape=(None))]: - """ - The transparency color of frame that is treated as transparent, 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 - """ - - @transparency.setter - def transparency(self, arg: Annotated[ArrayLike, dict(dtype='uint8', shape=(None))], /) -> None: ... - - @property - def palette_size(self) -> int: - """The palette data size of frame.""" - - @palette_size.setter - def palette_size(self, arg: int, /) -> None: ... - - @property - def transparency_size(self) -> int: - """The transparency data size of frame.""" - - @transparency_size.setter - def transparency_size(self, arg: int, /) -> None: ... - - @property - def delay_num(self) -> int: - """ - The nominator of the duration of frame. Duration of time is delay_num / delay_den seconds. - """ - - @delay_num.setter - def delay_num(self, arg: int, /) -> None: ... - - @property - def delay_den(self) -> int: - """ - The denominator of the duration of frame. Duration of time is delay_num / delay_den seconds. - """ - - @delay_den.setter - def delay_den(self, arg: int, /) -> None: ... - -class IAPNGAsmListener: - """Class for APNGAsmListener. Meant to be used internally.""" -def create_frame_from_rgb(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 3))], width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> APNGFrame: - """ - Creates an APNGFrame from a bitmapped array of RBG pixel data. - - :param numpy.typing.NDArray pixels: The RGB pixel data, expressed as 3D numpy array. - :param int width: The width of the pixel data. - :param int height: The height of the pixel data. - :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). - :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - - :return: A APNGFrame object. - :rtype: apngasm_python._apngasm_python.APNGFrame - """ - -def create_frame_from_rgb_trns(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 3))], width: int, height: int, trns_color: Annotated[ArrayLike, dict(dtype='uint8', shape=(3))], delay_num: int = 100, delay_den: int = 1000) -> APNGFrame: - """ - Creates an APNGFrame from a bitmapped array of RBG pixel data, with one color treated as transparent. - - :param numpy.typing.NDArray pixels: The RGB pixel data, expressed as 3D numpy array. - :param int width: The width of the pixel data. - :param int height: The height of the pixel data. - :param numpy.typing.NDArray trns_color: The color [r, g, b] to be treated as transparent, expressed as 1D numpy array. - :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). - :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - - :return: A APNGFrame object. - :rtype: apngasm_python._apngasm_python.APNGFrame - """ - -def create_frame_from_rgba(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 4))], width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> APNGFrame: - """ - Creates an APNGFrame from a bitmapped array of RBGA pixel data. - - :param numpy.typing.NDArray pixels: The RGBA pixel data, expressed as 3D numpy array. - :param int width: The width of the pixel data. - :param int height: The height of the pixel data. - :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR) - :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR) - - :return: A APNGFrame object. - :rtype: apngasm_python._apngasm_python.APNGFrame - """ - -class rgb: - """Class for RGB object. Meant to be used internally.""" - @overload - def __init__(self) -> None: - """Create an empty RGB object. Meant to be used internally.""" - - @overload - def __init__(self, arg0: int, arg1: int, arg2: int, /) -> None: - """Create a RGB object. Meant to be used internally.""" - - @property - def r(self) -> int: ... - - @r.setter - def r(self, arg: int, /) -> None: ... - - @property - def g(self) -> int: ... - - @g.setter - def g(self, arg: int, /) -> None: ... - - @property - def b(self) -> int: ... - - @b.setter - def b(self, arg: int, /) -> None: ... - -class rgba: - """Class for RGBA object. Meant to be used internally.""" - @overload - def __init__(self) -> None: - """Create an empty RGBA object. Meant to be used internally.""" - - @overload - def __init__(self, arg0: int, arg1: int, arg2: int, arg3: int, /) -> None: - """Create a RGBA object. Meant to be used internally.""" - - @property - def r(self) -> int: ... - - @r.setter - def r(self, arg: int, /) -> None: ... - - @property - def g(self) -> int: ... - - @g.setter - def g(self, arg: int, /) -> None: ... - - @property - def b(self) -> int: ... - - @b.setter - def b(self, arg: int, /) -> None: ... - - @property - def a(self) -> int: ... - - @a.setter - def a(self, arg: int, /) -> None: ... diff --git a/src-python/apngasm_python/apngasm.py b/src-python/apngasm_python/apngasm.py index f95a934..28b0863 100755 --- a/src-python/apngasm_python/apngasm.py +++ b/src-python/apngasm_python/apngasm.py @@ -13,7 +13,6 @@ create_frame_from_rgb, create_frame_from_rgb_trns, create_frame_from_rgba, - __version__, # type: ignore ) From 5d9407de5f1bc726e84625ad21d7579d43fe9780 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sun, 18 Feb 2024 20:08:03 +0800 Subject: [PATCH 012/103] WIP: Fix commit check --- scripts/get_deps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/get_deps.py b/scripts/get_deps.py index 5c740ac..5d42c71 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -4,7 +4,7 @@ import subprocess import shutil -from scripts.get_arch import conan_archs, get_arch +from get_arch import conan_archs, get_arch def install_deps(arch: str): From 516e3a5322528939955a0ccad6b5e291475bb35f Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sun, 18 Feb 2024 20:09:41 +0800 Subject: [PATCH 013/103] WIP: Fix commit check --- .github/workflows/check.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 0f00107..81200e6 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -22,11 +22,11 @@ jobs: run: | pip install build ruff pip install git+https://github.com/wjakob/nanobind.git@stubgen - ruff check ./src-python - ruff check ./scripts - python -m build . - ruff format ./src-python - ruff format ./scripts + ruff check src-python + ruff check scripts + pip install . + ruff format src-python + ruff format scripts python -m nanobind.stubgen \ -m apngasm_python \ -o src-python/apngasm_python/_apngasm_python.pyi \ From d55013788b24378f2c991bc860edec52fe2d3e34 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sun, 18 Feb 2024 20:13:58 +0800 Subject: [PATCH 014/103] WIP: Only run commit check on latest commit --- .github/workflows/check.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 81200e6..783de54 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -3,6 +3,10 @@ name: Update stub on: push: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: stubgen: runs-on: ubuntu-latest @@ -18,7 +22,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.8' - - name: Check + - name: Check, update stub and formatting run: | pip install build ruff pip install git+https://github.com/wjakob/nanobind.git@stubgen From 076e7b8db7bfaeb8bb6c4476d735fad39698bdec Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 18 Feb 2024 12:19:53 +0000 Subject: [PATCH 015/103] Update stub and formatting --- CMakeUserPresets.json | 9 +++++++++ src-python/apngasm_python/_apngasm_python.pyi | 1 + src-python/apngasm_python/apngasm.py | 2 +- src-python/apngasm_python/py.typed | 0 4 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 CMakeUserPresets.json create mode 100644 src-python/apngasm_python/_apngasm_python.pyi create mode 100644 src-python/apngasm_python/py.typed diff --git a/CMakeUserPresets.json b/CMakeUserPresets.json new file mode 100644 index 0000000..32bded8 --- /dev/null +++ b/CMakeUserPresets.json @@ -0,0 +1,9 @@ +{ + "version": 4, + "vendor": { + "conan": {} + }, + "include": [ + "/home/runner/work/apngasm-python/apngasm-python/conan_output/x86_64/CMakePresets.json" + ] +} \ No newline at end of file diff --git a/src-python/apngasm_python/_apngasm_python.pyi b/src-python/apngasm_python/_apngasm_python.pyi new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src-python/apngasm_python/_apngasm_python.pyi @@ -0,0 +1 @@ + diff --git a/src-python/apngasm_python/apngasm.py b/src-python/apngasm_python/apngasm.py index 28b0863..1ff1a8f 100755 --- a/src-python/apngasm_python/apngasm.py +++ b/src-python/apngasm_python/apngasm.py @@ -6,7 +6,7 @@ from numpy.typing import NDArray from PIL import Image -from ._apngasm_python import ( # type: ignore +from ._apngasm_python import ( # type: ignore APNGAsm, APNGFrame, IAPNGAsmListener, diff --git a/src-python/apngasm_python/py.typed b/src-python/apngasm_python/py.typed new file mode 100644 index 0000000..e69de29 From 572237a514e7d2e054d13a57f64ca5c47831a129 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sun, 18 Feb 2024 20:20:42 +0800 Subject: [PATCH 016/103] Fix commit action --- .github/workflows/check.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 783de54..189820f 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -1,4 +1,4 @@ -name: Update stub +name: Check, update stub and formatting on: push: @@ -32,7 +32,7 @@ jobs: ruff format src-python ruff format scripts python -m nanobind.stubgen \ - -m apngasm_python \ + -m apngasm_python._apngasm_python \ -o src-python/apngasm_python/_apngasm_python.pyi \ -M src-python/apngasm_python/py.typed - name: Commit & Push changes From 4ddaa936cf9c90275fe6804aa1b2bd3773b95261 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sun, 18 Feb 2024 20:22:05 +0800 Subject: [PATCH 017/103] Rename action --- .github/workflows/check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 189820f..1de91c1 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -8,7 +8,7 @@ concurrency: cancel-in-progress: true jobs: - stubgen: + check_and_fix: runs-on: ubuntu-latest steps: - uses: actions/checkout@master From 4be9b2cfbabb3f619aca621339e2ecfe26170373 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 18 Feb 2024 12:27:59 +0000 Subject: [PATCH 018/103] Update stub and formatting --- src-python/apngasm_python/_apngasm_python.pyi | 473 ++++++++++++++++++ 1 file changed, 473 insertions(+) diff --git a/src-python/apngasm_python/_apngasm_python.pyi b/src-python/apngasm_python/_apngasm_python.pyi index 8b13789..08afa82 100644 --- a/src-python/apngasm_python/_apngasm_python.pyi +++ b/src-python/apngasm_python/_apngasm_python.pyi @@ -1 +1,474 @@ +from numpy.typing import ArrayLike +from typing import overload, Sequence, Optional, List, Annotated +class APNGAsm: + """Class representing APNG file, storing APNGFrame(s) and other metadata.""" + @overload + def __init__(self) -> None: + """Construct an empty APNGAsm object.""" + + @overload + def __init__(self, frames: Sequence[APNGFrame]) -> None: + """ + Construct APNGAsm object from an existing vector of apngasm frames. + + :param list[apngasm_python._apngasm_python.APNGFrame] frames: A list of APNGFrame objects. + """ + + def add_frame(self, frame: APNGFrame) -> int: + """ + Adds an APNGFrame object to the frame vector. + + :param frame: The APNGFrame object to be added. + :type frame: apngasm_python._apngasm_python.APNGFrame + + :return: The new number of frames/the number of this frame on the frame vector. + :rtype: 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 DEFAULT_FRAME_NUMERATOR). + :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). + + :return: The new number of frames/the number of this frame on the frame vector. + :rtype: int + """ + + def add_frame_from_rgb(self, pixels_rgb: rgb, width: int, height: int, trns_color: Optional[rgb] = 0, delay_num: int = 100, delay_den: int = 1000) -> int: + """ + Adds an APNGFrame object to the vector. + Not possible to use in Python. As alternative, + Use create_frame_from_rgb() or create_frame_from_rgba(). Or manually, + First create an empty APNGFrame with frame = APNGFrame(), + then set frame.width, frame.height, frame.color_type, frame.pixels, + frame.palette, frame.delay_num, frame.delay_den manually. + + :param apngasm_python._apngasm_python.rgb pixels_rgb: The RGB pixel data. + :param int width: The width of the pixel data. + :param int height: The height of the pixel data. + :param apngasm_python._apngasm_python.rgb trns_color: The color [r, g, b] to be treated as transparent. + :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). + :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). + + :return: The new number of frames/the number of this frame on the frame vector. + :rtype: int + """ + + def add_frame_from_rgba(self, pixels_rgba: rgba, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> int: + """ + Adds an APNGFrame object to the vector. + Not possible to use in Python. As alternative, + Use create_frame_from_rgb() or create_frame_from_rgba(). Or manually, + First create an empty APNGFrame with frame = APNGFrame(), + then set frame.width, frame.height, frame.color_type, frame.pixels, + frame.palette, frame.delay_num, frame.delay_den manually. + + :param apngasm_python._apngasm_python.rgba pixels_rgba: The RGBA pixel data. + :param int width: The width of the pixel data. + :param int height: The height of the pixel data. + :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). + :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). + + :return: The new number of frames/the number of this frame on the frame vector. + :rtype: int + """ + + 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 + """ + + def disassemble(self, file_path: str) -> List[APNGFrame]: + """ + Disassembles an APNG file. + + :param str file_path: The file path to the PNG image to be disassembled. + + :return: A vector containing the frames of the disassembled PNG. + :rtype: list[apngasm_python._apngasm_python.APNGFrame] + """ + + 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 + """ + + 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: + https://github.com/Genshin/PhantomStandards + + :param str file_path: The path of JSON or XML file. + + :return: A vector containing the frames + :rtype: list[apngasm_python._apngasm_python.APNGFrame] + """ + + def save_json(self, output_path: str, image_dir: str) -> bool: + """ + Saves a JSON animation spec file. + + :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 + """ + + def save_xml(self, output_path: str, image_dir: str) -> bool: + """ + Saves an XML animation spec file. + + :param str file_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 + """ + + def set_apngasm_listener(self, listener: Optional[IAPNGAsmListener] = None) -> None: + """ + Sets a listener. + + :param Optional[apngasm_python._apngasm_python.IAPNGAsmListener] listener: A pointer to the listener object. If the argument is NULL a default APNGAsmListener will be created and assigned. + """ + + def set_loops(self, loops: int = 0) -> None: + """ + Set loop count of animation. + + :param int loops: Loop count of animation. If the argument is 0 a loop count is infinity. + """ + + def set_skip_first(self, skip_first: bool) -> None: + """ + Set flag of skip first frame. + + :param int skip_first: Flag of skip first frame. + """ + + def get_frames(self) -> List[APNGFrame]: + """ + Returns the frame vector. + + :return: frame vector. + :rtype: list[apngasm_python._apngasm_python.APNGFrame] + """ + + def get_loops(self) -> int: + """ + Returns the loop count. + + :return: loop count. + :rtype: int + """ + + def is_skip_first(self) -> bool: + """ + Returns the flag of skip first frame. + + :return: flag of skip first frame. + :rtype: bool + """ + + def frame_count(self) -> int: + """ + Returns the number of frames. + + :return: number of frames. + :rtype: int + """ + + 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 + """ + + def version(self) -> str: + """ + Returns the version of APNGAsm. + + :return: the version of APNGAsm. + :rtype: str + """ + +class APNGFrame: + """Class representing a frame in APNG.""" + @overload + def __init__(self) -> None: + """Creates an empty APNGFrame.""" + + @overload + def __init__(self, file_path: str, delay_num: int = 100, delay_den: int = 1000) -> None: + """ + Creates an APNGFrame from a PNG file. + + :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 DEFAULT_FRAME_NUMERATOR). + :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). + """ + + @overload + def __init__(self, pixels: rgb, width: int, height: int, trns_color: rgb, delay_num: int = 100, delay_den: int = 1000) -> None: + """ + Creates an APNGFrame from a bitmapped array of RBG pixel data. + Not possible to use in Python. To create APNGFrame from pixel data in memory, + Use create_frame_from_rgb() or create_frame_from_rgba(). Or manually, + First create an empty APNGFrame with frame = APNGFrame(), + then set frame.width, frame.height, frame.color_type, frame.pixels, + frame.palette, frame.delay_num, frame.delay_den manually. + + :param apngasm_python._apngasm_python.rgb pixels: The RGB pixel data. + :param int width: The width of the pixel data. + :param int height: The height of the pixel data. + :param apngasm_python._apngasm_python.rgb trns_color: The color [r, g, b] to be treated as transparent. + :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). + :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). + """ + + @overload + def __init__(self, pixels: rgba, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> None: + """ + Creates an APNGFrame from a bitmapped array of RBGA pixel data. + Not possible to use in Python. To create APNGFrame from pixel data in memory, + Use create_frame_from_rgb() or create_frame_from_rgba(). Or manually, + First create an empty APNGFrame with frame = APNGFrame(), + then set frame.width, frame.height, frame.color_type, frame.pixels, + frame.palette, frame.delay_num, frame.delay_den manually. + + :param apngasm_python._apngasm_python.rgba pixels: The RGBA pixel data. + :param int width: The width of the pixel data. + :param int height: The height of the pixel data. + :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). + :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). + """ + + def save(self, out_path: str) -> bool: + """ + Saves this frame as a single PNG file. + + :param str out_path: The relative or absolute path to save the image file to. + + :return: true if save was successful. + :rtype: bool + """ + + @property + def pixels(self) -> Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, None))]: + """ + The raw pixel data of frame, expressed as a 3D numpy array in Python. + Note that setting this value will also set the variable 'rows' internally. + This should be set AFTER you set the width, height and color_type. + """ + + @pixels.setter + def pixels(self, arg: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, None))], /) -> None: ... + + @property + def width(self) -> int: + """The width of frame.""" + + @width.setter + def width(self, arg: int, /) -> None: ... + + @property + def height(self) -> int: + """The height of frame.""" + + @height.setter + def height(self, arg: int, /) -> None: ... + + @property + def color_type(self) -> int: + """ + The color_type of the frame. + + 0: Grayscale (Pillow mode='L') + 2: RGB (Pillow mode='RGB') + 3: Palette (Pillow mode='P') + 4: Grayscale + Alpha (Pillow mode='LA') + 6: RGBA (Pillow mode='RGBA') + """ + + @color_type.setter + def color_type(self, arg: int, /) -> None: ... + + @property + def palette(self) -> Annotated[ArrayLike, dict(dtype='uint8', shape=(256, 3))]: + """ + 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]] in Python. + """ + + @palette.setter + def palette(self, arg: Annotated[ArrayLike, dict(dtype='uint8', shape=(256, 3))], /) -> None: ... + + @property + def transparency(self) -> Annotated[ArrayLike, dict(dtype='uint8', shape=(None))]: + """ + The transparency color of frame that is treated as transparent, 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 + """ + + @transparency.setter + def transparency(self, arg: Annotated[ArrayLike, dict(dtype='uint8', shape=(None))], /) -> None: ... + + @property + def palette_size(self) -> int: + """The palette data size of frame.""" + + @palette_size.setter + def palette_size(self, arg: int, /) -> None: ... + + @property + def transparency_size(self) -> int: + """The transparency data size of frame.""" + + @transparency_size.setter + def transparency_size(self, arg: int, /) -> None: ... + + @property + def delay_num(self) -> int: + """ + The nominator of the duration of frame. Duration of time is delay_num / delay_den seconds. + """ + + @delay_num.setter + def delay_num(self, arg: int, /) -> None: ... + + @property + def delay_den(self) -> int: + """ + The denominator of the duration of frame. Duration of time is delay_num / delay_den seconds. + """ + + @delay_den.setter + def delay_den(self, arg: int, /) -> None: ... + +class IAPNGAsmListener: + """Class for APNGAsmListener. Meant to be used internally.""" +def create_frame_from_rgb(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 3))], width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> APNGFrame: + """ + Creates an APNGFrame from a bitmapped array of RBG pixel data. + + :param numpy.typing.NDArray pixels: The RGB pixel data, expressed as 3D numpy array. + :param int width: The width of the pixel data. + :param int height: The height of the pixel data. + :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). + :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). + + :return: A APNGFrame object. + :rtype: apngasm_python._apngasm_python.APNGFrame + """ + +def create_frame_from_rgb_trns(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 3))], width: int, height: int, trns_color: Annotated[ArrayLike, dict(dtype='uint8', shape=(3))], delay_num: int = 100, delay_den: int = 1000) -> APNGFrame: + """ + Creates an APNGFrame from a bitmapped array of RBG pixel data, with one color treated as transparent. + + :param numpy.typing.NDArray pixels: The RGB pixel data, expressed as 3D numpy array. + :param int width: The width of the pixel data. + :param int height: The height of the pixel data. + :param numpy.typing.NDArray trns_color: The color [r, g, b] to be treated as transparent, expressed as 1D numpy array. + :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). + :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). + + :return: A APNGFrame object. + :rtype: apngasm_python._apngasm_python.APNGFrame + """ + +def create_frame_from_rgba(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 4))], width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> APNGFrame: + """ + Creates an APNGFrame from a bitmapped array of RBGA pixel data. + + :param numpy.typing.NDArray pixels: The RGBA pixel data, expressed as 3D numpy array. + :param int width: The width of the pixel data. + :param int height: The height of the pixel data. + :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR) + :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR) + + :return: A APNGFrame object. + :rtype: apngasm_python._apngasm_python.APNGFrame + """ + +class rgb: + """Class for RGB object. Meant to be used internally.""" + @overload + def __init__(self) -> None: + """Create an empty RGB object. Meant to be used internally.""" + + @overload + def __init__(self, arg0: int, arg1: int, arg2: int, /) -> None: + """Create a RGB object. Meant to be used internally.""" + + @property + def r(self) -> int: ... + + @r.setter + def r(self, arg: int, /) -> None: ... + + @property + def g(self) -> int: ... + + @g.setter + def g(self, arg: int, /) -> None: ... + + @property + def b(self) -> int: ... + + @b.setter + def b(self, arg: int, /) -> None: ... + +class rgba: + """Class for RGBA object. Meant to be used internally.""" + @overload + def __init__(self) -> None: + """Create an empty RGBA object. Meant to be used internally.""" + + @overload + def __init__(self, arg0: int, arg1: int, arg2: int, arg3: int, /) -> None: + """Create a RGBA object. Meant to be used internally.""" + + @property + def r(self) -> int: ... + + @r.setter + def r(self, arg: int, /) -> None: ... + + @property + def g(self) -> int: ... + + @g.setter + def g(self, arg: int, /) -> None: ... + + @property + def b(self) -> int: ... + + @b.setter + def b(self, arg: int, /) -> None: ... + + @property + def a(self) -> int: ... + + @a.setter + def a(self, arg: int, /) -> None: ... From cd53506fd7ad4be65cae429fa0a186a6c291321f Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sun, 18 Feb 2024 21:51:14 +0800 Subject: [PATCH 019/103] - Only build required boost components - Update dependencies - Add gitignore --- .gitignore | 3 ++- CMakeUserPresets.json | 9 --------- conanfile.py | 8 ++++---- scripts/get_deps.py | 39 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 44 insertions(+), 15 deletions(-) delete mode 100644 CMakeUserPresets.json diff --git a/.gitignore b/.gitignore index 958e8c3..510a616 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ dist/ conan_output/ example/output/ venv/ -docs/_build/ \ No newline at end of file +docs/_build/ +CMakeUserPresets.json \ No newline at end of file diff --git a/CMakeUserPresets.json b/CMakeUserPresets.json deleted file mode 100644 index 32bded8..0000000 --- a/CMakeUserPresets.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "version": 4, - "vendor": { - "conan": {} - }, - "include": [ - "/home/runner/work/apngasm-python/apngasm-python/conan_output/x86_64/CMakePresets.json" - ] -} \ No newline at end of file diff --git a/conanfile.py b/conanfile.py index 5a0eace..24b3f96 100644 --- a/conanfile.py +++ b/conanfile.py @@ -11,11 +11,11 @@ class ApngasmRecipe(ConanFile): settings = "os", "compiler", "build_type", "arch" def requirements(self): - self.requires("zlib/1.2.13") - self.requires("libpng/1.6.40") + self.requires("zlib/1.3.1") + self.requires("libpng/1.6.42") self.requires( - "boost/1.75.0" - ) # https://github.com/conan-io/conan-center-index/issues/19704 + "boost/1.84.0" + ) def build_requirements(self): self.build_requires("b2/4.10.1") diff --git a/scripts/get_deps.py b/scripts/get_deps.py index 5d42c71..5fda3a8 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -10,6 +10,8 @@ def install_deps(arch: str): # Use Conan to install dependencies settings: list[str] = [] + build: list[str] = [] + options: list[str] = [] if platform.system() == "Windows": settings.append("os=Windows") @@ -29,8 +31,41 @@ def install_deps(arch: str): settings.append("compiler.libcxx=libstdc++") if arch: settings.append("arch=" + arch) + + options.append("boost/*:without_atomic=True") + options.append("boost/*:without_chrono=True") + options.append("boost/*:without_cobalt=True") + options.append("boost/*:without_container=True") + options.append("boost/*:without_context=True") + options.append("boost/*:without_contract=True") + options.append("boost/*:without_coroutine=True") + options.append("boost/*:without_date_time=True") + options.append("boost/*:without_exception=True") + options.append("boost/*:without_fiber=True") + options.append("boost/*:without_filesystem=True") + options.append("boost/*:without_graph=True") + options.append("boost/*:without_graph_parallel=True") + options.append("boost/*:without_iostreams=True") + options.append("boost/*:without_json=True") + options.append("boost/*:without_locale=True") + options.append("boost/*:without_log=True") + options.append("boost/*:without_math=True") + options.append("boost/*:without_mpi=True") + options.append("boost/*:without_nowide=True") + options.append("boost/*:without_program_options=False") + options.append("boost/*:without_python=True") + options.append("boost/*:without_random=True") + options.append("boost/*:without_regex=False") + options.append("boost/*:without_serialization=True") + options.append("boost/*:without_stacktrace=True") + options.append("boost/*:without_system=False") + options.append("boost/*:without_test=True") + options.append("boost/*:without_thread=True") + options.append("boost/*:without_timer=True") + options.append("boost/*:without_type_erasure=True") + options.append("boost/*:without_url=True") + options.append("boost/*:without_wave=True") - build: list[str] = [] if platform.system() == "Linux": # Need to compile dependencies if Linux build.append("*") @@ -46,6 +81,7 @@ def install_deps(arch: str): print("conan cli settings:") print("settings: " + str(settings)) print("build: " + str(build)) + print("options: " + str(options)) subprocess.run(["conan", "profile", "detect"]) @@ -57,6 +93,7 @@ def install_deps(arch: str): "install", *[x for s in settings for x in ("-s", s)], *[x for b in build for x in ("-b", b)], + *[x for o in options for x in ("-o", o)], "-of", conan_output, "--deployer=direct_deploy", From 44ecbb60f75104fd90bb58e3062d2a07949ac654 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 18 Feb 2024 13:54:54 +0000 Subject: [PATCH 020/103] Update stub and formatting --- scripts/get_deps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/get_deps.py b/scripts/get_deps.py index 5fda3a8..969b86b 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -31,7 +31,7 @@ def install_deps(arch: str): settings.append("compiler.libcxx=libstdc++") if arch: settings.append("arch=" + arch) - + options.append("boost/*:without_atomic=True") options.append("boost/*:without_chrono=True") options.append("boost/*:without_cobalt=True") From b9a068c2b029601812bd13d506247cc0a0e2fc13 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sun, 18 Feb 2024 21:55:38 +0800 Subject: [PATCH 021/103] Formatting --- conanfile.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/conanfile.py b/conanfile.py index 24b3f96..bc65415 100644 --- a/conanfile.py +++ b/conanfile.py @@ -13,9 +13,7 @@ class ApngasmRecipe(ConanFile): def requirements(self): self.requires("zlib/1.3.1") self.requires("libpng/1.6.42") - self.requires( - "boost/1.84.0" - ) + self.requires("boost/1.84.0") def build_requirements(self): self.build_requires("b2/4.10.1") From cfd801b62c257892c117304a031749d9d95274b9 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sun, 18 Feb 2024 22:21:26 +0800 Subject: [PATCH 022/103] - Cleaning docstring - Rename commit action name --- .../{check.yml => check_and_fix.yml} | 3 +- src-python/apngasm_python/apngasm.py | 67 +++++++++++-------- 2 files changed, 40 insertions(+), 30 deletions(-) rename .github/workflows/{check.yml => check_and_fix.yml} (95%) diff --git a/.github/workflows/check.yml b/.github/workflows/check_and_fix.yml similarity index 95% rename from .github/workflows/check.yml rename to .github/workflows/check_and_fix.yml index 1de91c1..1d2e8c2 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check_and_fix.yml @@ -24,10 +24,11 @@ jobs: python-version: '3.8' - name: Check, update stub and formatting run: | - pip install build ruff + pip install build ruff pyright pip install git+https://github.com/wjakob/nanobind.git@stubgen ruff check src-python ruff check scripts + # pyright pip install . ruff format src-python ruff format scripts diff --git a/src-python/apngasm_python/apngasm.py b/src-python/apngasm_python/apngasm.py index 1ff1a8f..5e38e13 100755 --- a/src-python/apngasm_python/apngasm.py +++ b/src-python/apngasm_python/apngasm.py @@ -41,8 +41,8 @@ def frame_pixels_as_pillow( 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. + :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] @@ -71,10 +71,11 @@ def frame_pixels_as_numpy( This should be set AFTER you set the width, height and color_type. :param int frame: Target frame number. - :param Optional[numpy.typing.NDArray[Any]] new_value: If set, then the raw pixel data of frame - is set with this value. + :param Optional[numpy.typing.NDArray[Any]] 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) + :return: 3D numpy array representation of + raw pixel data of frame (get) or None (set) :rtype: Optional[numpy.typing.NDArray[Any]] """ from numpy import array @@ -146,14 +147,17 @@ def frame_palette( self, frame: int, new_value: Optional[NDArray[Any]] = None ) -> Optional[NDArray[Any]]: """ - 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]] + 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[Any]] new_value: If set, then the palette data of frame - is set with this value. + :param Optional[numpy.typing.NDArray[Any]] 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) + :return: 2D numpy array representation of + palette data of frame (get) or None (set) :rtype: Optional[numpy.typing.NDArray[Any]] """ from numpy import array @@ -167,15 +171,17 @@ def frame_transparency( self, frame: int, new_value: Optional[NDArray[Any]] = None ) -> Optional[NDArray[Any]]: """ - Get/Set the color [r, g, b] to be treated as transparent in the frame, expressed as 1D numpy array. + 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[Any]] new_value: If set, then the transparency of frame - is set with this value. + :param Optional[numpy.typing.NDArray[Any]] 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) + :return: The color [r, g, b] to be treated as transparent + in the frame (get) or None (set) :rtype: Optional[numpy.typing.NDArray[Any]] """ from numpy import array @@ -229,8 +235,8 @@ def frame_delay_num( 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. + :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] @@ -248,8 +254,8 @@ def frame_delay_den( 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. + :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] @@ -319,14 +325,17 @@ def add_frame_from_numpy( 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[Any] numpy_data: The pixel data, expressed as 3D numpy array. + :param numpy.typing.NDArray[Any] 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[Any]] trns_color: The color [r, g, b] to be treated as transparent, expressed as 1D numpy array. + :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[Any]] 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). @@ -334,8 +343,7 @@ def add_frame_from_numpy( :return: The new number of frames. :rtype: int """ - from numpy import shape - from numpy.typing import NDArray + from numpy import shape, ndarray width = width if width else shape(numpy_data)[1] height = height if height else shape(numpy_data)[0] @@ -354,7 +362,7 @@ def add_frame_from_numpy( ) if mode == "RGB": - if type(trns_color) == NDArray: + if isinstance(trns_color, ndarray): frame = create_frame_from_rgb_trns( pixels=numpy_data, width=width, @@ -372,7 +380,7 @@ def add_frame_from_numpy( delay_den=delay_den, ) elif mode == "RGBA": - if type(trns_color) == NDArray: + if isinstance(trns_color, ndarray): raise TypeError( "Cannot set trns_color on RGBA mode Pillow object. Must be RGB." ) @@ -497,8 +505,8 @@ 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, + :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. """ raise NotImplementedError("set_apng_asm_listener is not implemented") @@ -508,7 +516,8 @@ 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. + :param int loops: Loop count of animation. If the argument is 0 + a loop count is infinity. """ return self.apngasm.set_loops(loops) From 473d466cd34df31b5a7f20d79309df0ed4c50a68 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 18 Feb 2024 14:25:22 +0000 Subject: [PATCH 023/103] Update stub and formatting --- src-python/apngasm_python/apngasm.py | 40 ++++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src-python/apngasm_python/apngasm.py b/src-python/apngasm_python/apngasm.py index 5e38e13..1f82661 100755 --- a/src-python/apngasm_python/apngasm.py +++ b/src-python/apngasm_python/apngasm.py @@ -41,7 +41,7 @@ def frame_pixels_as_pillow( 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 + :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) @@ -71,10 +71,10 @@ def frame_pixels_as_numpy( This should be set AFTER you set the width, height and color_type. :param int frame: Target frame number. - :param Optional[numpy.typing.NDArray[Any]] new_value: If set, then the + :param Optional[numpy.typing.NDArray[Any]] new_value: If set, then the raw pixel data of frame is set with this value. - :return: 3D numpy array representation of + :return: 3D numpy array representation of raw pixel data of frame (get) or None (set) :rtype: Optional[numpy.typing.NDArray[Any]] """ @@ -147,16 +147,16 @@ def frame_palette( self, frame: int, new_value: Optional[NDArray[Any]] = None ) -> Optional[NDArray[Any]]: """ - Get/Set the palette data of frame. - Only applies to 'P' mode Image (i.e. Not RGB, RGBA). - Expressed as 2D numpy array + 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[Any]] new_value: If set, then + :param Optional[numpy.typing.NDArray[Any]] new_value: If set, then the palette data of frame is set with this value. - :return: 2D numpy array representation of + :return: 2D numpy array representation of palette data of frame (get) or None (set) :rtype: Optional[numpy.typing.NDArray[Any]] """ @@ -171,13 +171,13 @@ def frame_transparency( self, frame: int, new_value: Optional[NDArray[Any]] = None ) -> Optional[NDArray[Any]]: """ - Get/Set the color [r, g, b] to be treated as transparent in the frame, - expressed as 1D numpy array. + 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[Any]] new_value: If set, then the + :param Optional[numpy.typing.NDArray[Any]] 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 @@ -235,7 +235,7 @@ def frame_delay_num( 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 + :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. @@ -254,7 +254,7 @@ def frame_delay_den( 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 + :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. @@ -325,17 +325,17 @@ def add_frame_from_numpy( 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[Any] numpy_data: The pixel data, expressed as + :param numpy.typing.NDArray[Any] 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 + :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[Any]] trns_color: The color [r, g, b] to - be treated as transparent, expressed as 1D numpy array. + :param Optional[numpy.typing.NDArray[Any]] 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). @@ -505,7 +505,7 @@ 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: + :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. """ @@ -516,7 +516,7 @@ def set_loops(self, loops: int = 0): """ Set loop count of animation. - :param int loops: Loop count of animation. If the argument is 0 + :param int loops: Loop count of animation. If the argument is 0 a loop count is infinity. """ return self.apngasm.set_loops(loops) From 436746c5122d42fae60291b26111654dbabd11db Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sun, 18 Feb 2024 22:37:48 +0800 Subject: [PATCH 024/103] 1.3.0 --- CMakeLists.txt | 2 +- src-python/apngasm_python/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d21d9c8..5f2be91 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.3) +project(apngasm-python VERSION 1.3.0) set(PY_VERSION_SUFFIX "") set(PY_FULL_VERSION ${PROJECT_VERSION}${PY_VERSION_SUFFIX}) diff --git a/src-python/apngasm_python/__init__.py b/src-python/apngasm_python/__init__.py index 56fca9e..225e5e5 100755 --- a/src-python/apngasm_python/__init__.py +++ b/src-python/apngasm_python/__init__.py @@ -1,3 +1,3 @@ #!/usr/bin/env python3 """apngasm-python""" -__version__ = "1.2.3" +__version__ = "1.3.0" From 1b36e90ae68ec7d5fa5b5e5693a9249bbcaf1678 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sun, 18 Feb 2024 22:38:02 +0800 Subject: [PATCH 025/103] Test building wheels for all platform --- .github/workflows/build.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4b9927a..379a835 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -139,18 +139,18 @@ jobs: # password: ${{ secrets.TEST_PYPI_API_TOKEN }} # repository_url: https://test.pypi.org/legacy/ - upload_pypi: - needs: [build_wheels, build_sdist] - runs-on: ubuntu-latest - steps: - - uses: actions/download-artifact@v3 - with: - # unpacks default artifact into dist/ - # if `name: artifact` is omitted, the action will create extra parent dir - name: artifact - path: dist + # upload_pypi: + # needs: [build_wheels, build_sdist] + # runs-on: ubuntu-latest + # steps: + # - uses: actions/download-artifact@v3 + # with: + # # unpacks default artifact into dist/ + # # if `name: artifact` is omitted, the action will create extra parent dir + # name: artifact + # path: dist - - uses: pypa/gh-action-pypi-publish@v1.5.0 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file + # - uses: pypa/gh-action-pypi-publish@v1.5.0 + # with: + # user: __token__ + # password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file From a8240dae57249dc1c3bd48f9af0a319a5f60d26b Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sun, 18 Feb 2024 22:41:12 +0800 Subject: [PATCH 026/103] Reverse disabling upload_pypi --- .github/workflows/build.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 379a835..4b9927a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -139,18 +139,18 @@ jobs: # password: ${{ secrets.TEST_PYPI_API_TOKEN }} # repository_url: https://test.pypi.org/legacy/ - # upload_pypi: - # needs: [build_wheels, build_sdist] - # runs-on: ubuntu-latest - # steps: - # - uses: actions/download-artifact@v3 - # with: - # # unpacks default artifact into dist/ - # # if `name: artifact` is omitted, the action will create extra parent dir - # name: artifact - # path: dist + upload_pypi: + needs: [build_wheels, build_sdist] + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v3 + with: + # unpacks default artifact into dist/ + # if `name: artifact` is omitted, the action will create extra parent dir + name: artifact + path: dist - # - uses: pypa/gh-action-pypi-publish@v1.5.0 - # with: - # user: __token__ - # password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file + - uses: pypa/gh-action-pypi-publish@v1.5.0 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file From 9eab726f5f42aa5a027848a693e97ba5cf95da0f Mon Sep 17 00:00:00 2001 From: laggykiller Date: Mon, 19 Feb 2024 10:30:36 +0800 Subject: [PATCH 027/103] Fix stub typing --- CMakeLists.txt | 4 +- src-python/apngasm_python/_apngasm_python.pyi | 474 ------------------ src/apngasm_python.cpp | 2 +- 3 files changed, 3 insertions(+), 477 deletions(-) delete mode 100644 src-python/apngasm_python/_apngasm_python.pyi diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f2be91..c6e6e20 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -117,8 +117,8 @@ install(TARGETS _apngasm_python nanobind_add_stub( apngasm_python_stub INSTALL_TIME - MODULE apngasm_python - OUTPUT "${PY_BUILD_CMAKE_MODULE_NAME}/__init__.pyi" + MODULE apngasm_python._apngasm_python + OUTPUT "${PY_BUILD_CMAKE_MODULE_NAME}/_apngasm_python.pyi" MARKER_FILE "${PY_BUILD_CMAKE_MODULE_NAME}/py.typed" COMPONENT python_modules EXCLUDE_FROM_ALL diff --git a/src-python/apngasm_python/_apngasm_python.pyi b/src-python/apngasm_python/_apngasm_python.pyi deleted file mode 100644 index 08afa82..0000000 --- a/src-python/apngasm_python/_apngasm_python.pyi +++ /dev/null @@ -1,474 +0,0 @@ -from numpy.typing import ArrayLike -from typing import overload, Sequence, Optional, List, Annotated - -class APNGAsm: - """Class representing APNG file, storing APNGFrame(s) and other metadata.""" - @overload - def __init__(self) -> None: - """Construct an empty APNGAsm object.""" - - @overload - def __init__(self, frames: Sequence[APNGFrame]) -> None: - """ - Construct APNGAsm object from an existing vector of apngasm frames. - - :param list[apngasm_python._apngasm_python.APNGFrame] frames: A list of APNGFrame objects. - """ - - def add_frame(self, frame: APNGFrame) -> int: - """ - Adds an APNGFrame object to the frame vector. - - :param frame: The APNGFrame object to be added. - :type frame: apngasm_python._apngasm_python.APNGFrame - - :return: The new number of frames/the number of this frame on the frame vector. - :rtype: 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 DEFAULT_FRAME_NUMERATOR). - :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - - :return: The new number of frames/the number of this frame on the frame vector. - :rtype: int - """ - - def add_frame_from_rgb(self, pixels_rgb: rgb, width: int, height: int, trns_color: Optional[rgb] = 0, delay_num: int = 100, delay_den: int = 1000) -> int: - """ - Adds an APNGFrame object to the vector. - Not possible to use in Python. As alternative, - Use create_frame_from_rgb() or create_frame_from_rgba(). Or manually, - First create an empty APNGFrame with frame = APNGFrame(), - then set frame.width, frame.height, frame.color_type, frame.pixels, - frame.palette, frame.delay_num, frame.delay_den manually. - - :param apngasm_python._apngasm_python.rgb pixels_rgb: The RGB pixel data. - :param int width: The width of the pixel data. - :param int height: The height of the pixel data. - :param apngasm_python._apngasm_python.rgb trns_color: The color [r, g, b] to be treated as transparent. - :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). - :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - - :return: The new number of frames/the number of this frame on the frame vector. - :rtype: int - """ - - def add_frame_from_rgba(self, pixels_rgba: rgba, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> int: - """ - Adds an APNGFrame object to the vector. - Not possible to use in Python. As alternative, - Use create_frame_from_rgb() or create_frame_from_rgba(). Or manually, - First create an empty APNGFrame with frame = APNGFrame(), - then set frame.width, frame.height, frame.color_type, frame.pixels, - frame.palette, frame.delay_num, frame.delay_den manually. - - :param apngasm_python._apngasm_python.rgba pixels_rgba: The RGBA pixel data. - :param int width: The width of the pixel data. - :param int height: The height of the pixel data. - :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). - :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - - :return: The new number of frames/the number of this frame on the frame vector. - :rtype: int - """ - - 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 - """ - - def disassemble(self, file_path: str) -> List[APNGFrame]: - """ - Disassembles an APNG file. - - :param str file_path: The file path to the PNG image to be disassembled. - - :return: A vector containing the frames of the disassembled PNG. - :rtype: list[apngasm_python._apngasm_python.APNGFrame] - """ - - 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 - """ - - 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: - https://github.com/Genshin/PhantomStandards - - :param str file_path: The path of JSON or XML file. - - :return: A vector containing the frames - :rtype: list[apngasm_python._apngasm_python.APNGFrame] - """ - - def save_json(self, output_path: str, image_dir: str) -> bool: - """ - Saves a JSON animation spec file. - - :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 - """ - - def save_xml(self, output_path: str, image_dir: str) -> bool: - """ - Saves an XML animation spec file. - - :param str file_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 - """ - - def set_apngasm_listener(self, listener: Optional[IAPNGAsmListener] = None) -> None: - """ - Sets a listener. - - :param Optional[apngasm_python._apngasm_python.IAPNGAsmListener] listener: A pointer to the listener object. If the argument is NULL a default APNGAsmListener will be created and assigned. - """ - - def set_loops(self, loops: int = 0) -> None: - """ - Set loop count of animation. - - :param int loops: Loop count of animation. If the argument is 0 a loop count is infinity. - """ - - def set_skip_first(self, skip_first: bool) -> None: - """ - Set flag of skip first frame. - - :param int skip_first: Flag of skip first frame. - """ - - def get_frames(self) -> List[APNGFrame]: - """ - Returns the frame vector. - - :return: frame vector. - :rtype: list[apngasm_python._apngasm_python.APNGFrame] - """ - - def get_loops(self) -> int: - """ - Returns the loop count. - - :return: loop count. - :rtype: int - """ - - def is_skip_first(self) -> bool: - """ - Returns the flag of skip first frame. - - :return: flag of skip first frame. - :rtype: bool - """ - - def frame_count(self) -> int: - """ - Returns the number of frames. - - :return: number of frames. - :rtype: int - """ - - 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 - """ - - def version(self) -> str: - """ - Returns the version of APNGAsm. - - :return: the version of APNGAsm. - :rtype: str - """ - -class APNGFrame: - """Class representing a frame in APNG.""" - @overload - def __init__(self) -> None: - """Creates an empty APNGFrame.""" - - @overload - def __init__(self, file_path: str, delay_num: int = 100, delay_den: int = 1000) -> None: - """ - Creates an APNGFrame from a PNG file. - - :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 DEFAULT_FRAME_NUMERATOR). - :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - """ - - @overload - def __init__(self, pixels: rgb, width: int, height: int, trns_color: rgb, delay_num: int = 100, delay_den: int = 1000) -> None: - """ - Creates an APNGFrame from a bitmapped array of RBG pixel data. - Not possible to use in Python. To create APNGFrame from pixel data in memory, - Use create_frame_from_rgb() or create_frame_from_rgba(). Or manually, - First create an empty APNGFrame with frame = APNGFrame(), - then set frame.width, frame.height, frame.color_type, frame.pixels, - frame.palette, frame.delay_num, frame.delay_den manually. - - :param apngasm_python._apngasm_python.rgb pixels: The RGB pixel data. - :param int width: The width of the pixel data. - :param int height: The height of the pixel data. - :param apngasm_python._apngasm_python.rgb trns_color: The color [r, g, b] to be treated as transparent. - :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). - :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - """ - - @overload - def __init__(self, pixels: rgba, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> None: - """ - Creates an APNGFrame from a bitmapped array of RBGA pixel data. - Not possible to use in Python. To create APNGFrame from pixel data in memory, - Use create_frame_from_rgb() or create_frame_from_rgba(). Or manually, - First create an empty APNGFrame with frame = APNGFrame(), - then set frame.width, frame.height, frame.color_type, frame.pixels, - frame.palette, frame.delay_num, frame.delay_den manually. - - :param apngasm_python._apngasm_python.rgba pixels: The RGBA pixel data. - :param int width: The width of the pixel data. - :param int height: The height of the pixel data. - :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). - :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - """ - - def save(self, out_path: str) -> bool: - """ - Saves this frame as a single PNG file. - - :param str out_path: The relative or absolute path to save the image file to. - - :return: true if save was successful. - :rtype: bool - """ - - @property - def pixels(self) -> Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, None))]: - """ - The raw pixel data of frame, expressed as a 3D numpy array in Python. - Note that setting this value will also set the variable 'rows' internally. - This should be set AFTER you set the width, height and color_type. - """ - - @pixels.setter - def pixels(self, arg: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, None))], /) -> None: ... - - @property - def width(self) -> int: - """The width of frame.""" - - @width.setter - def width(self, arg: int, /) -> None: ... - - @property - def height(self) -> int: - """The height of frame.""" - - @height.setter - def height(self, arg: int, /) -> None: ... - - @property - def color_type(self) -> int: - """ - The color_type of the frame. - - 0: Grayscale (Pillow mode='L') - 2: RGB (Pillow mode='RGB') - 3: Palette (Pillow mode='P') - 4: Grayscale + Alpha (Pillow mode='LA') - 6: RGBA (Pillow mode='RGBA') - """ - - @color_type.setter - def color_type(self, arg: int, /) -> None: ... - - @property - def palette(self) -> Annotated[ArrayLike, dict(dtype='uint8', shape=(256, 3))]: - """ - 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]] in Python. - """ - - @palette.setter - def palette(self, arg: Annotated[ArrayLike, dict(dtype='uint8', shape=(256, 3))], /) -> None: ... - - @property - def transparency(self) -> Annotated[ArrayLike, dict(dtype='uint8', shape=(None))]: - """ - The transparency color of frame that is treated as transparent, 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 - """ - - @transparency.setter - def transparency(self, arg: Annotated[ArrayLike, dict(dtype='uint8', shape=(None))], /) -> None: ... - - @property - def palette_size(self) -> int: - """The palette data size of frame.""" - - @palette_size.setter - def palette_size(self, arg: int, /) -> None: ... - - @property - def transparency_size(self) -> int: - """The transparency data size of frame.""" - - @transparency_size.setter - def transparency_size(self, arg: int, /) -> None: ... - - @property - def delay_num(self) -> int: - """ - The nominator of the duration of frame. Duration of time is delay_num / delay_den seconds. - """ - - @delay_num.setter - def delay_num(self, arg: int, /) -> None: ... - - @property - def delay_den(self) -> int: - """ - The denominator of the duration of frame. Duration of time is delay_num / delay_den seconds. - """ - - @delay_den.setter - def delay_den(self, arg: int, /) -> None: ... - -class IAPNGAsmListener: - """Class for APNGAsmListener. Meant to be used internally.""" -def create_frame_from_rgb(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 3))], width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> APNGFrame: - """ - Creates an APNGFrame from a bitmapped array of RBG pixel data. - - :param numpy.typing.NDArray pixels: The RGB pixel data, expressed as 3D numpy array. - :param int width: The width of the pixel data. - :param int height: The height of the pixel data. - :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). - :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - - :return: A APNGFrame object. - :rtype: apngasm_python._apngasm_python.APNGFrame - """ - -def create_frame_from_rgb_trns(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 3))], width: int, height: int, trns_color: Annotated[ArrayLike, dict(dtype='uint8', shape=(3))], delay_num: int = 100, delay_den: int = 1000) -> APNGFrame: - """ - Creates an APNGFrame from a bitmapped array of RBG pixel data, with one color treated as transparent. - - :param numpy.typing.NDArray pixels: The RGB pixel data, expressed as 3D numpy array. - :param int width: The width of the pixel data. - :param int height: The height of the pixel data. - :param numpy.typing.NDArray trns_color: The color [r, g, b] to be treated as transparent, expressed as 1D numpy array. - :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). - :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). - - :return: A APNGFrame object. - :rtype: apngasm_python._apngasm_python.APNGFrame - """ - -def create_frame_from_rgba(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 4))], width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> APNGFrame: - """ - Creates an APNGFrame from a bitmapped array of RBGA pixel data. - - :param numpy.typing.NDArray pixels: The RGBA pixel data, expressed as 3D numpy array. - :param int width: The width of the pixel data. - :param int height: The height of the pixel data. - :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR) - :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR) - - :return: A APNGFrame object. - :rtype: apngasm_python._apngasm_python.APNGFrame - """ - -class rgb: - """Class for RGB object. Meant to be used internally.""" - @overload - def __init__(self) -> None: - """Create an empty RGB object. Meant to be used internally.""" - - @overload - def __init__(self, arg0: int, arg1: int, arg2: int, /) -> None: - """Create a RGB object. Meant to be used internally.""" - - @property - def r(self) -> int: ... - - @r.setter - def r(self, arg: int, /) -> None: ... - - @property - def g(self) -> int: ... - - @g.setter - def g(self, arg: int, /) -> None: ... - - @property - def b(self) -> int: ... - - @b.setter - def b(self, arg: int, /) -> None: ... - -class rgba: - """Class for RGBA object. Meant to be used internally.""" - @overload - def __init__(self) -> None: - """Create an empty RGBA object. Meant to be used internally.""" - - @overload - def __init__(self, arg0: int, arg1: int, arg2: int, arg3: int, /) -> None: - """Create a RGBA object. Meant to be used internally.""" - - @property - def r(self) -> int: ... - - @r.setter - def r(self, arg: int, /) -> None: ... - - @property - def g(self) -> int: ... - - @g.setter - def g(self, arg: int, /) -> None: ... - - @property - def b(self) -> int: ... - - @b.setter - def b(self, arg: int, /) -> None: ... - - @property - def a(self) -> int: ... - - @a.setter - def a(self, arg: int, /) -> None: ... diff --git a/src/apngasm_python.cpp b/src/apngasm_python.cpp index e07fcfa..c0492a8 100644 --- a/src/apngasm_python.cpp +++ b/src/apngasm_python.cpp @@ -423,7 +423,7 @@ NB_MODULE(MODULE_NAME, m) { )pbdoc") .def("add_frame_from_rgb", nb::overload_cast(&apngasm::APNGAsm::addFrame), - "pixels_rgb"_a, "width"_a, "height"_a, "trns_color"_a.none() = NULL, "delay_num"_a = apngasm::DEFAULT_FRAME_NUMERATOR, "delay_den"_a = apngasm::DEFAULT_FRAME_DENOMINATOR, + "pixels_rgb"_a, "width"_a, "height"_a, "trns_color"_a.none(), "delay_num"_a = apngasm::DEFAULT_FRAME_NUMERATOR, "delay_den"_a = apngasm::DEFAULT_FRAME_DENOMINATOR, R"pbdoc( Adds an APNGFrame object to the vector. Not possible to use in Python. As alternative, From a022b802590f0b8fb54e8c455fd3f78eb9cc625f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 19 Feb 2024 02:33:46 +0000 Subject: [PATCH 028/103] Update stub and formatting --- src-python/apngasm_python/_apngasm_python.pyi | 474 ++++++++++++++++++ 1 file changed, 474 insertions(+) create mode 100644 src-python/apngasm_python/_apngasm_python.pyi diff --git a/src-python/apngasm_python/_apngasm_python.pyi b/src-python/apngasm_python/_apngasm_python.pyi new file mode 100644 index 0000000..3b4c4cc --- /dev/null +++ b/src-python/apngasm_python/_apngasm_python.pyi @@ -0,0 +1,474 @@ +from numpy.typing import ArrayLike +from typing import overload, Sequence, Optional, List, Annotated + +class APNGAsm: + """Class representing APNG file, storing APNGFrame(s) and other metadata.""" + @overload + def __init__(self) -> None: + """Construct an empty APNGAsm object.""" + + @overload + def __init__(self, frames: Sequence[APNGFrame]) -> None: + """ + Construct APNGAsm object from an existing vector of apngasm frames. + + :param list[apngasm_python._apngasm_python.APNGFrame] frames: A list of APNGFrame objects. + """ + + def add_frame(self, frame: APNGFrame) -> int: + """ + Adds an APNGFrame object to the frame vector. + + :param frame: The APNGFrame object to be added. + :type frame: apngasm_python._apngasm_python.APNGFrame + + :return: The new number of frames/the number of this frame on the frame vector. + :rtype: 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 DEFAULT_FRAME_NUMERATOR). + :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). + + :return: The new number of frames/the number of this frame on the frame vector. + :rtype: int + """ + + def add_frame_from_rgb(self, pixels_rgb: rgb, width: int, height: int, trns_color: Optional[rgb], delay_num: int = 100, delay_den: int = 1000) -> int: + """ + Adds an APNGFrame object to the vector. + Not possible to use in Python. As alternative, + Use create_frame_from_rgb() or create_frame_from_rgba(). Or manually, + First create an empty APNGFrame with frame = APNGFrame(), + then set frame.width, frame.height, frame.color_type, frame.pixels, + frame.palette, frame.delay_num, frame.delay_den manually. + + :param apngasm_python._apngasm_python.rgb pixels_rgb: The RGB pixel data. + :param int width: The width of the pixel data. + :param int height: The height of the pixel data. + :param apngasm_python._apngasm_python.rgb trns_color: The color [r, g, b] to be treated as transparent. + :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). + :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). + + :return: The new number of frames/the number of this frame on the frame vector. + :rtype: int + """ + + def add_frame_from_rgba(self, pixels_rgba: rgba, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> int: + """ + Adds an APNGFrame object to the vector. + Not possible to use in Python. As alternative, + Use create_frame_from_rgb() or create_frame_from_rgba(). Or manually, + First create an empty APNGFrame with frame = APNGFrame(), + then set frame.width, frame.height, frame.color_type, frame.pixels, + frame.palette, frame.delay_num, frame.delay_den manually. + + :param apngasm_python._apngasm_python.rgba pixels_rgba: The RGBA pixel data. + :param int width: The width of the pixel data. + :param int height: The height of the pixel data. + :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). + :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). + + :return: The new number of frames/the number of this frame on the frame vector. + :rtype: int + """ + + 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 + """ + + def disassemble(self, file_path: str) -> List[APNGFrame]: + """ + Disassembles an APNG file. + + :param str file_path: The file path to the PNG image to be disassembled. + + :return: A vector containing the frames of the disassembled PNG. + :rtype: list[apngasm_python._apngasm_python.APNGFrame] + """ + + 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 + """ + + 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: + https://github.com/Genshin/PhantomStandards + + :param str file_path: The path of JSON or XML file. + + :return: A vector containing the frames + :rtype: list[apngasm_python._apngasm_python.APNGFrame] + """ + + def save_json(self, output_path: str, image_dir: str) -> bool: + """ + Saves a JSON animation spec file. + + :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 + """ + + def save_xml(self, output_path: str, image_dir: str) -> bool: + """ + Saves an XML animation spec file. + + :param str file_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 + """ + + def set_apngasm_listener(self, listener: Optional[IAPNGAsmListener] = None) -> None: + """ + Sets a listener. + + :param Optional[apngasm_python._apngasm_python.IAPNGAsmListener] listener: A pointer to the listener object. If the argument is NULL a default APNGAsmListener will be created and assigned. + """ + + def set_loops(self, loops: int = 0) -> None: + """ + Set loop count of animation. + + :param int loops: Loop count of animation. If the argument is 0 a loop count is infinity. + """ + + def set_skip_first(self, skip_first: bool) -> None: + """ + Set flag of skip first frame. + + :param int skip_first: Flag of skip first frame. + """ + + def get_frames(self) -> List[APNGFrame]: + """ + Returns the frame vector. + + :return: frame vector. + :rtype: list[apngasm_python._apngasm_python.APNGFrame] + """ + + def get_loops(self) -> int: + """ + Returns the loop count. + + :return: loop count. + :rtype: int + """ + + def is_skip_first(self) -> bool: + """ + Returns the flag of skip first frame. + + :return: flag of skip first frame. + :rtype: bool + """ + + def frame_count(self) -> int: + """ + Returns the number of frames. + + :return: number of frames. + :rtype: int + """ + + 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 + """ + + def version(self) -> str: + """ + Returns the version of APNGAsm. + + :return: the version of APNGAsm. + :rtype: str + """ + +class APNGFrame: + """Class representing a frame in APNG.""" + @overload + def __init__(self) -> None: + """Creates an empty APNGFrame.""" + + @overload + def __init__(self, file_path: str, delay_num: int = 100, delay_den: int = 1000) -> None: + """ + Creates an APNGFrame from a PNG file. + + :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 DEFAULT_FRAME_NUMERATOR). + :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). + """ + + @overload + def __init__(self, pixels: rgb, width: int, height: int, trns_color: rgb, delay_num: int = 100, delay_den: int = 1000) -> None: + """ + Creates an APNGFrame from a bitmapped array of RBG pixel data. + Not possible to use in Python. To create APNGFrame from pixel data in memory, + Use create_frame_from_rgb() or create_frame_from_rgba(). Or manually, + First create an empty APNGFrame with frame = APNGFrame(), + then set frame.width, frame.height, frame.color_type, frame.pixels, + frame.palette, frame.delay_num, frame.delay_den manually. + + :param apngasm_python._apngasm_python.rgb pixels: The RGB pixel data. + :param int width: The width of the pixel data. + :param int height: The height of the pixel data. + :param apngasm_python._apngasm_python.rgb trns_color: The color [r, g, b] to be treated as transparent. + :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). + :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). + """ + + @overload + def __init__(self, pixels: rgba, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> None: + """ + Creates an APNGFrame from a bitmapped array of RBGA pixel data. + Not possible to use in Python. To create APNGFrame from pixel data in memory, + Use create_frame_from_rgb() or create_frame_from_rgba(). Or manually, + First create an empty APNGFrame with frame = APNGFrame(), + then set frame.width, frame.height, frame.color_type, frame.pixels, + frame.palette, frame.delay_num, frame.delay_den manually. + + :param apngasm_python._apngasm_python.rgba pixels: The RGBA pixel data. + :param int width: The width of the pixel data. + :param int height: The height of the pixel data. + :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). + :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). + """ + + def save(self, out_path: str) -> bool: + """ + Saves this frame as a single PNG file. + + :param str out_path: The relative or absolute path to save the image file to. + + :return: true if save was successful. + :rtype: bool + """ + + @property + def pixels(self) -> Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, None))]: + """ + The raw pixel data of frame, expressed as a 3D numpy array in Python. + Note that setting this value will also set the variable 'rows' internally. + This should be set AFTER you set the width, height and color_type. + """ + + @pixels.setter + def pixels(self, arg: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, None))], /) -> None: ... + + @property + def width(self) -> int: + """The width of frame.""" + + @width.setter + def width(self, arg: int, /) -> None: ... + + @property + def height(self) -> int: + """The height of frame.""" + + @height.setter + def height(self, arg: int, /) -> None: ... + + @property + def color_type(self) -> int: + """ + The color_type of the frame. + + 0: Grayscale (Pillow mode='L') + 2: RGB (Pillow mode='RGB') + 3: Palette (Pillow mode='P') + 4: Grayscale + Alpha (Pillow mode='LA') + 6: RGBA (Pillow mode='RGBA') + """ + + @color_type.setter + def color_type(self, arg: int, /) -> None: ... + + @property + def palette(self) -> Annotated[ArrayLike, dict(dtype='uint8', shape=(256, 3))]: + """ + 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]] in Python. + """ + + @palette.setter + def palette(self, arg: Annotated[ArrayLike, dict(dtype='uint8', shape=(256, 3))], /) -> None: ... + + @property + def transparency(self) -> Annotated[ArrayLike, dict(dtype='uint8', shape=(None))]: + """ + The transparency color of frame that is treated as transparent, 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 + """ + + @transparency.setter + def transparency(self, arg: Annotated[ArrayLike, dict(dtype='uint8', shape=(None))], /) -> None: ... + + @property + def palette_size(self) -> int: + """The palette data size of frame.""" + + @palette_size.setter + def palette_size(self, arg: int, /) -> None: ... + + @property + def transparency_size(self) -> int: + """The transparency data size of frame.""" + + @transparency_size.setter + def transparency_size(self, arg: int, /) -> None: ... + + @property + def delay_num(self) -> int: + """ + The nominator of the duration of frame. Duration of time is delay_num / delay_den seconds. + """ + + @delay_num.setter + def delay_num(self, arg: int, /) -> None: ... + + @property + def delay_den(self) -> int: + """ + The denominator of the duration of frame. Duration of time is delay_num / delay_den seconds. + """ + + @delay_den.setter + def delay_den(self, arg: int, /) -> None: ... + +class IAPNGAsmListener: + """Class for APNGAsmListener. Meant to be used internally.""" +def create_frame_from_rgb(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 3))], width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> APNGFrame: + """ + Creates an APNGFrame from a bitmapped array of RBG pixel data. + + :param numpy.typing.NDArray pixels: The RGB pixel data, expressed as 3D numpy array. + :param int width: The width of the pixel data. + :param int height: The height of the pixel data. + :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). + :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). + + :return: A APNGFrame object. + :rtype: apngasm_python._apngasm_python.APNGFrame + """ + +def create_frame_from_rgb_trns(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 3))], width: int, height: int, trns_color: Annotated[ArrayLike, dict(dtype='uint8', shape=(3))], delay_num: int = 100, delay_den: int = 1000) -> APNGFrame: + """ + Creates an APNGFrame from a bitmapped array of RBG pixel data, with one color treated as transparent. + + :param numpy.typing.NDArray pixels: The RGB pixel data, expressed as 3D numpy array. + :param int width: The width of the pixel data. + :param int height: The height of the pixel data. + :param numpy.typing.NDArray trns_color: The color [r, g, b] to be treated as transparent, expressed as 1D numpy array. + :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR). + :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR). + + :return: A APNGFrame object. + :rtype: apngasm_python._apngasm_python.APNGFrame + """ + +def create_frame_from_rgba(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 4))], width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> APNGFrame: + """ + Creates an APNGFrame from a bitmapped array of RBGA pixel data. + + :param numpy.typing.NDArray pixels: The RGBA pixel data, expressed as 3D numpy array. + :param int width: The width of the pixel data. + :param int height: The height of the pixel data. + :param int delay_num: The delay numerator for this frame (defaults to DEFAULT_FRAME_NUMERATOR) + :param int delay_den: The delay denominator for this frame (defaults to DEFAULT_FRAME_DENMINATOR) + + :return: A APNGFrame object. + :rtype: apngasm_python._apngasm_python.APNGFrame + """ + +class rgb: + """Class for RGB object. Meant to be used internally.""" + @overload + def __init__(self) -> None: + """Create an empty RGB object. Meant to be used internally.""" + + @overload + def __init__(self, arg0: int, arg1: int, arg2: int, /) -> None: + """Create a RGB object. Meant to be used internally.""" + + @property + def r(self) -> int: ... + + @r.setter + def r(self, arg: int, /) -> None: ... + + @property + def g(self) -> int: ... + + @g.setter + def g(self, arg: int, /) -> None: ... + + @property + def b(self) -> int: ... + + @b.setter + def b(self, arg: int, /) -> None: ... + +class rgba: + """Class for RGBA object. Meant to be used internally.""" + @overload + def __init__(self) -> None: + """Create an empty RGBA object. Meant to be used internally.""" + + @overload + def __init__(self, arg0: int, arg1: int, arg2: int, arg3: int, /) -> None: + """Create a RGBA object. Meant to be used internally.""" + + @property + def r(self) -> int: ... + + @r.setter + def r(self, arg: int, /) -> None: ... + + @property + def g(self) -> int: ... + + @g.setter + def g(self, arg: int, /) -> None: ... + + @property + def b(self) -> int: ... + + @b.setter + def b(self, arg: int, /) -> None: ... + + @property + def a(self) -> int: ... + + @a.setter + def a(self, arg: int, /) -> None: ... From 282375b641ed00a2e25de60831316c1a1333032d Mon Sep 17 00:00:00 2001 From: laggykiller Date: Fri, 23 Feb 2024 23:59:22 +0800 Subject: [PATCH 029/103] Add lipo-dir-merge in source distribution --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cd35106..e5c305f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,8 @@ include = [ "src-python/*", "scripts/*", "conanfile.py", - "apngasm/*" + "apngasm/*", + "lipo-dir-merge/*.py" ] [tool.py-build-cmake.cmake] # How to build the CMake project From e33dbcbf77064d013bc19faa291818824ad07e30 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sat, 24 Feb 2024 03:41:56 +0800 Subject: [PATCH 030/103] Using master branch of nanobind --- .github/workflows/check_and_fix.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check_and_fix.yml b/.github/workflows/check_and_fix.yml index 1d2e8c2..0bc9d8f 100644 --- a/.github/workflows/check_and_fix.yml +++ b/.github/workflows/check_and_fix.yml @@ -25,7 +25,7 @@ jobs: - name: Check, update stub and formatting run: | pip install build ruff pyright - pip install git+https://github.com/wjakob/nanobind.git@stubgen + pip install git+https://github.com/wjakob/nanobind.git ruff check src-python ruff check scripts # pyright diff --git a/pyproject.toml b/pyproject.toml index e5c305f..3af9a03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ Tracker = "https://github.com/laggykiller/apngasm-python/issues" [build-system] # How pip and other frontends should build this project requires = [ "py-build-cmake==0.2.0a7", - "nanobind @ git+https://github.com/wjakob/nanobind.git@stubgen", + "nanobind @ git+https://github.com/wjakob/nanobind.git", "conan>=2.0", "wheel" ] From 8f6e2c47579673e2461c8f9b6b6765d36ba8ebf8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 23 Feb 2024 19:44:45 +0000 Subject: [PATCH 031/103] Update stub and formatting --- src-python/apngasm_python/_apngasm_python.pyi | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src-python/apngasm_python/_apngasm_python.pyi b/src-python/apngasm_python/_apngasm_python.pyi index 3b4c4cc..4351721 100644 --- a/src-python/apngasm_python/_apngasm_python.pyi +++ b/src-python/apngasm_python/_apngasm_python.pyi @@ -3,6 +3,7 @@ from typing import overload, Sequence, Optional, List, Annotated class APNGAsm: """Class representing APNG file, storing APNGFrame(s) and other metadata.""" + @overload def __init__(self) -> None: """Construct an empty APNGAsm object.""" @@ -215,6 +216,7 @@ class APNGAsm: class APNGFrame: """Class representing a frame in APNG.""" + @overload def __init__(self) -> None: """Creates an empty APNGFrame.""" @@ -368,6 +370,7 @@ class APNGFrame: class IAPNGAsmListener: """Class for APNGAsmListener. Meant to be used internally.""" + def create_frame_from_rgb(pixels: Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 3))], width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> APNGFrame: """ Creates an APNGFrame from a bitmapped array of RBG pixel data. @@ -413,6 +416,7 @@ def create_frame_from_rgba(pixels: Annotated[ArrayLike, dict(dtype='uint8', shap class rgb: """Class for RGB object. Meant to be used internally.""" + @overload def __init__(self) -> None: """Create an empty RGB object. Meant to be used internally.""" @@ -441,6 +445,7 @@ class rgb: class rgba: """Class for RGBA object. Meant to be used internally.""" + @overload def __init__(self) -> None: """Create an empty RGBA object. Meant to be used internally.""" From 78092b805094dd563994a2671fc2cae17c8cd324 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Tue, 27 Feb 2024 13:17:04 +0800 Subject: [PATCH 032/103] Fix is_skip_first binded to incorrect function in binder --- src-python/apngasm_python/apngasm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src-python/apngasm_python/apngasm.py b/src-python/apngasm_python/apngasm.py index 1f82661..baa9c9b 100755 --- a/src-python/apngasm_python/apngasm.py +++ b/src-python/apngasm_python/apngasm.py @@ -547,14 +547,14 @@ def get_loops(self) -> int: """ return self.apngasm.get_loops() - def is_skip_first(self) -> int: + def is_skip_first(self) -> bool: """ Returns the flag of skip first frame. :return: flag of skip first frame. - :rtype: int + :rtype: bool """ - return self.apngasm.get_loops() + return self.apngasm.is_skip_first() def frame_count(self) -> int: """ From 596432e273c5042454390d5747610d5c7a9f08b9 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Tue, 27 Feb 2024 13:23:01 +0800 Subject: [PATCH 033/103] Safer check for non-rgb frame given to add_frame_from_numpy --- src-python/apngasm_python/apngasm.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src-python/apngasm_python/apngasm.py b/src-python/apngasm_python/apngasm.py index baa9c9b..99a1e7a 100755 --- a/src-python/apngasm_python/apngasm.py +++ b/src-python/apngasm_python/apngasm.py @@ -349,16 +349,17 @@ def add_frame_from_numpy( height = height if height else shape(numpy_data)[0] if not mode: - if shape(numpy_data)[2] == 3: - mode = "RGB" - elif shape(numpy_data)[2] == 4: - mode = "RGBA" + if len(shape(numpy_data)) == 3: + if shape(numpy_data)[2] == 3: + mode = "RGB" + elif 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). " - "The given numpy_data 3rd dimension size was " - f"{shape(numpy_data)[2]}." + "The given numpy_data shape was " + f"{shape(numpy_data)}." ) if mode == "RGB": From 033033760d583567cea82a0cff5c002a0d61ecd3 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Tue, 27 Feb 2024 13:41:38 +0800 Subject: [PATCH 034/103] fix apngasm_listener typo in binder --- src-python/apngasm_python/apngasm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src-python/apngasm_python/apngasm.py b/src-python/apngasm_python/apngasm.py index 99a1e7a..218b385 100755 --- a/src-python/apngasm_python/apngasm.py +++ b/src-python/apngasm_python/apngasm.py @@ -501,7 +501,7 @@ def save_xml(self, output_path: str, image_dir: str) -> bool: """ return self.apngasm.save_xml(output_path, image_dir) - def set_apng_asm_listener(self, listener: Optional[IAPNGAsmListener] = None): # type: ignore + def set_apngasm_listener(self, listener: Optional[IAPNGAsmListener] = None): # type: ignore """ Sets a listener. You probably won't need to use this function. @@ -510,8 +510,8 @@ def set_apng_asm_listener(self, listener: Optional[IAPNGAsmListener] = None): # A pointer to the listener object. If the argument is None, a default APNGAsmListener will be created and assigned. """ - raise NotImplementedError("set_apng_asm_listener is not implemented") - # return self.apngasm.set_apng_asm_listener(listener) + raise NotImplementedError("set_apngasm_listener is not implemented") + # return self.apngasm.set_apngasm_listener(listener) def set_loops(self, loops: int = 0): """ From 46697c05bb67f95eb20441e0d0f85c6146a1ad6f Mon Sep 17 00:00:00 2001 From: laggykiller Date: Tue, 27 Feb 2024 13:49:09 +0800 Subject: [PATCH 035/103] Add tests --- .github/workflows/build.yml | 40 +- .gitignore | 2 +- README.md | 36 +- example/example_binder.py | 58 ++- example/example_direct.py | 57 ++- {example => samples}/frames/elephant-001.png | Bin {example => samples}/frames/elephant-002.png | Bin {example => samples}/frames/elephant-003.png | Bin {example => samples}/frames/elephant-004.png | Bin {example => samples}/frames/elephant-005.png | Bin {example => samples}/frames/elephant-006.png | Bin {example => samples}/frames/elephant-007.png | Bin {example => samples}/frames/elephant-008.png | Bin {example => samples}/frames/elephant-009.png | Bin {example => samples}/frames/elephant-010.png | Bin {example => samples}/frames/elephant-011.png | Bin {example => samples}/frames/elephant-012.png | Bin {example => samples}/frames/elephant-013.png | Bin {example => samples}/frames/elephant-014.png | Bin {example => samples}/frames/elephant-015.png | Bin {example => samples}/frames/elephant-016.png | Bin {example => samples}/frames/elephant-017.png | Bin {example => samples}/frames/elephant-018.png | Bin {example => samples}/frames/elephant-019.png | Bin {example => samples}/frames/elephant-020.png | Bin {example => samples}/frames/elephant-021.png | Bin {example => samples}/frames/elephant-022.png | Bin {example => samples}/frames/elephant-023.png | Bin {example => samples}/frames/elephant-024.png | Bin {example => samples}/frames/elephant-025.png | Bin {example => samples}/frames/elephant-026.png | Bin {example => samples}/frames/elephant-027.png | Bin {example => samples}/frames/elephant-028.png | Bin {example => samples}/frames/elephant-029.png | Bin {example => samples}/frames/elephant-030.png | Bin {example => samples}/frames/elephant-031.png | Bin {example => samples}/frames/elephant-032.png | Bin {example => samples}/frames/elephant-033.png | Bin {example => samples}/frames/elephant-034.png | Bin example/input/grey.png => samples/input/0.png | Bin .../input/palette.png => samples/input/1.png | Bin samples/input/animation_spec.json | 12 + samples/input/animation_spec.xml | 2 + {example => samples}/input/ball.apng | Bin samples/input/grey.png | Bin 0 -> 20418 bytes samples/input/palette.png | Bin 0 -> 17680 bytes tests/test_binder.py | 395 ++++++++++++++++++ tests/test_direct.py | 288 +++++++++++++ 48 files changed, 814 insertions(+), 76 deletions(-) rename {example => samples}/frames/elephant-001.png (100%) rename {example => samples}/frames/elephant-002.png (100%) rename {example => samples}/frames/elephant-003.png (100%) rename {example => samples}/frames/elephant-004.png (100%) rename {example => samples}/frames/elephant-005.png (100%) rename {example => samples}/frames/elephant-006.png (100%) rename {example => samples}/frames/elephant-007.png (100%) rename {example => samples}/frames/elephant-008.png (100%) rename {example => samples}/frames/elephant-009.png (100%) rename {example => samples}/frames/elephant-010.png (100%) rename {example => samples}/frames/elephant-011.png (100%) rename {example => samples}/frames/elephant-012.png (100%) rename {example => samples}/frames/elephant-013.png (100%) rename {example => samples}/frames/elephant-014.png (100%) rename {example => samples}/frames/elephant-015.png (100%) rename {example => samples}/frames/elephant-016.png (100%) rename {example => samples}/frames/elephant-017.png (100%) rename {example => samples}/frames/elephant-018.png (100%) rename {example => samples}/frames/elephant-019.png (100%) rename {example => samples}/frames/elephant-020.png (100%) rename {example => samples}/frames/elephant-021.png (100%) rename {example => samples}/frames/elephant-022.png (100%) rename {example => samples}/frames/elephant-023.png (100%) rename {example => samples}/frames/elephant-024.png (100%) rename {example => samples}/frames/elephant-025.png (100%) rename {example => samples}/frames/elephant-026.png (100%) rename {example => samples}/frames/elephant-027.png (100%) rename {example => samples}/frames/elephant-028.png (100%) rename {example => samples}/frames/elephant-029.png (100%) rename {example => samples}/frames/elephant-030.png (100%) rename {example => samples}/frames/elephant-031.png (100%) rename {example => samples}/frames/elephant-032.png (100%) rename {example => samples}/frames/elephant-033.png (100%) rename {example => samples}/frames/elephant-034.png (100%) rename example/input/grey.png => samples/input/0.png (100%) rename example/input/palette.png => samples/input/1.png (100%) create mode 100644 samples/input/animation_spec.json create mode 100644 samples/input/animation_spec.xml rename {example => samples}/input/ball.apng (100%) create mode 100644 samples/input/grey.png create mode 100644 samples/input/palette.png create mode 100644 tests/test_binder.py create mode 100644 tests/test_direct.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4b9927a..54860ca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,6 +9,27 @@ on: - [published] jobs: + build_sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Build sdist + run: pipx run build --sdist + + - name: Test sdist + run: | + python -m pip install dist/apngasm_python-*.tar.gz + pip install pytest Pillow numpy && + pytest + + - uses: actions/upload-artifact@v3 + with: + path: dist/*.tar.gz + build_wheels: name: Build wheels for ${{ matrix.os }} ${{ matrix.cibw_archs }} ${{ matrix.cibw_build }} runs-on: ${{ matrix.os }} @@ -102,25 +123,14 @@ jobs: # CIBW_ENVIRONMENT: PY_BUILD_CMAKE_VERBOSE=1 ${{ matrix.cibw_environment }} CIBW_ENVIRONMENT: ${{ matrix.cibw_environment }} CIBW_BUILD: ${{ matrix.cibw_build }} + CIBW_TEST_REQUIRES: pytest + CIBW_BEFORE_TEST: pip install --only-binary ":all:" Pillow numpy; true + CIBW_BEFORE_TEST_WINDOWS: pip install --only-binary ":all:" Pillow numpy || VER>NUL + CIBW_TEST_COMMAND: pytest {package}/tests - uses: actions/upload-artifact@v3 with: path: ./wheelhouse/*.whl - - build_sdist: - name: Build source distribution - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Build sdist - run: pipx run build --sdist - - - uses: actions/upload-artifact@v3 - with: - path: dist/*.tar.gz # upload_pypi_test: # needs: [build_wheels, build_sdist] diff --git a/.gitignore b/.gitignore index 510a616..ff532c6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ dist/ .py-build-cmake_cache/ .vscode/ conan_output/ -example/output/ +samples/output/ venv/ docs/_build/ CMakeUserPresets.json \ No newline at end of file diff --git a/README.md b/README.md index c774b9f..95a885a 100644 --- a/README.md +++ b/README.md @@ -42,33 +42,33 @@ import os apngasm = APNGAsmBinder() # From file -for file_name in sorted(os.listdir('frames')): +for file_name in sorted(os.listdir("samples/frames")): # To adjust frame duration, set delay_num and delay_den # The frame duration will be (delay_num / delay_den) seconds - apngasm.add_frame_from_file(file_path=os.path.join('frames', file_name), delay_num=100, delay_den=1000) + apngasm.add_frame_from_file(file_path=os.path.join("samples/frames", file_name), delay_num=100, delay_den=1000) # Default value of loops is 0, which is infinite looping of APNG animation # This sets the APNG animation to loop for 3 times before stopping apngasm.set_loops(3) -apng.assemble('result-from-file.apng') +apng.assemble("samples/result-from-file.apng") apngasm.reset() # From Pillow -for file_name in sorted(os.listdir('frames')): - image = Image.open(os.path.join('frames', file_name)).convert('RGBA') +for file_name in sorted(os.listdir("samples/frames")): + image = Image.open(os.path.join("samples/frames", file_name)).convert("RGBA") frame = apngasm.add_frame_from_pillow(image, delay_num=50, delay_den=1000) -apngasm.assemble('result-from-pillow.apng') +apngasm.assemble("result-from-pillow.apng") apngasm.reset() # Disassemble and get pillow image of one frame # You can use with statement to avoid calling reset() with APNGAsmBinder() as apng: - frames = apng.disassemble_as_pillow('input/ball.apng') + frames = apng.disassemble_as_pillow("samples/input/ball.apng") frame = frames[0] - frame.save('output/ball0.png') + frame.save("samples/output/ball0.png") # Disassemble all APNG into PNGs -apngasm.save_pngs('output') +apngasm.save_pngs("samples/output") ``` Alternatively, you can reduce overhead and do advanced tasks by calling methods @@ -82,35 +82,35 @@ import os apngasm = APNGAsm() # From file -for file_name in sorted(os.listdir('frames')): +for file_name in sorted(os.listdir("samples/frames")): # To adjust frame duration, set delay_num and delay_den # The frame duration will be (delay_num / delay_den) seconds - apngasm.add_frame_from_file(file_path=os.path.join('frames', file_name), delay_num=100, delay_den=1000) + apngasm.add_frame_from_file(file_path=os.path.join("samples/frames", file_name), delay_num=100, delay_den=1000) # Default value of loops is 0, which is infinite looping of APNG animation # This sets the APNG animation to loop for 3 times before stopping apngasm.set_loops(3) -apng.assemble('result-from-file.apng') +apng.assemble("samples/result-from-file.apng") # From Pillow apngasm.reset() -for file_name in sorted(os.listdir('frames')): - image = Image.open(os.path.join('frames', file_name)).convert('RGBA') +for file_name in sorted(os.listdir("samples/frames")): + image = Image.open(os.path.join("samples/frames", file_name)).convert("RGBA") frame = create_frame_from_rgba(np.array(image), image.width, image.height) frame.delay_num = 50 frame.delay_den = 1000 apngasm.add_frame(frame) -apngasm.assemble('result-from-pillow.apng') +apngasm.assemble("samples/result-from-pillow.apng") # Disassemble and get pillow image of one frame apngasm.reset() -frames = apngasm.disassemble('input/ball.apng') +frames = apngasm.disassemble("samples/input/ball.apng") frame = frames[0] im = Image.frombytes(mode, (frame.width, frame.height), frame.pixels) -im.save('output/ball0.png') +im.save("samples/output/ball0.png") # Disassemble all APNG into PNGs -apngasm.save_pngs('output') +apngasm.save_pngs("samples/output") ``` The methods are based on [apngasm.h](https://github.com/apngasm/apngasm/blob/master/lib/src/apngasm.h) diff --git a/example/example_binder.py b/example/example_binder.py index 03070a6..59ba5d1 100755 --- a/example/example_binder.py +++ b/example/example_binder.py @@ -5,9 +5,18 @@ from PIL import Image import numpy as np +file_dir = os.path.split(__file__)[0] +samples_dir = os.path.join(file_dir, "../samples") +frames_dir = os.path.join(samples_dir, "frames") +input_dir = os.path.join(samples_dir, "input") +output_dir = os.path.join(samples_dir, "output") +ball_apng_path = os.path.join(input_dir, "ball.apng") +grey_png_path = os.path.join(input_dir, "grey.png") +palette_png_path = os.path.join(input_dir, "palette.png") + # Cleanup -shutil.rmtree("output", ignore_errors=True) -os.mkdir("output") +shutil.rmtree(output_dir, ignore_errors=True) +os.mkdir(output_dir) # Initialize apngasm = APNGAsmBinder() @@ -16,18 +25,20 @@ 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_dir)): + apngasm.add_frame_from_file(os.path.join(frames_dir, 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") +out = os.path.join(output_dir, "elephant-frame.png") +frame.save(out) # Getting one frame as Pillow Image im = apngasm.frame_pixels_as_pillow(0) -im.save("output/elephant-frame-pillow.png") # type: ignore +out = os.path.join(output_dir, "elephant-frame-pillow.png") +im.save(out) # type: ignore # Get inforamtion about whole animation print(f"{apngasm.get_loops() = }") @@ -35,53 +46,58 @@ print(f"{apngasm.frame_count() = }") # Assemble -success = apngasm.assemble("output/elephant.apng") +out = os.path.join(output_dir, "elephant.apng") +success = apngasm.assemble(out) 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(ball_apng_path) frame = frames[0] -frame.save("output/ball0.png") +out = os.path.join(output_dir, "ball0.png") +frame.save(out) # Disassemble all APNG into PNGs -apngasm.save_pngs("output") +apngasm.save_pngs(output_dir) # 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_dir)) +for file_name in sorted(os.listdir(frames_dir)): + image = Image.open(os.path.join(frames_dir, file_name)) image = image.rotate(angle) apngasm.add_frame_from_pillow(image) angle += angle_step -success = apngasm.assemble("output/elephant-spinning-pillow.apng") +out = os.path.join(output_dir, "elephant-spinning-pillow.apng") +success = apngasm.assemble(out) 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") + apng.add_frame_from_file(palette_png_path, delay_num=1, delay_den=1) + apng.add_frame_from_file(grey_png_path, delay_num=1, delay_den=1) + out = os.path.join(output_dir, "birds.apng") + success = apng.assemble(out) print(f"{success = }") # Assemble palette and grey PNGs, but with Pillow and numpy -image0 = Image.open("input/grey.png") +image0 = Image.open(grey_png_path) frame0 = apngasm.add_frame_from_pillow(image0, delay_num=1, delay_den=1) -image1 = Image.open("input/grey.png").convert("RGB") +image1 = Image.open(grey_png_path).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") +image2 = Image.open(palette_png_path) apngasm.add_frame_from_pillow(image2, delay_num=1, delay_den=1) -success = apngasm.assemble("output/birds-pillow.apng") +out = os.path.join(output_dir, "birds-pillow.apng") +success = apngasm.assemble(out) print(f"{success = }") diff --git a/example/example_direct.py b/example/example_direct.py index 2ce61ee..3fda2db 100755 --- a/example/example_direct.py +++ b/example/example_direct.py @@ -11,6 +11,14 @@ from PIL import Image import numpy as np +file_dir = os.path.split(__file__)[0] +samples_dir = os.path.join(file_dir, "../samples") +frames_dir = os.path.join(samples_dir, "frames") +input_dir = os.path.join(samples_dir, "input") +output_dir = os.path.join(samples_dir, "output") +ball_apng_path = os.path.join(input_dir, "ball.apng") +grey_png_path = os.path.join(input_dir, "grey.png") +palette_png_path = os.path.join(input_dir, "palette.png") def frame_info(frame: APNGFrame): print(f"{frame.pixels = }") @@ -31,8 +39,8 @@ def frame_info(frame: APNGFrame): color_type_dict.update(dict((v, k) for k, v in color_type_dict.items())) # type: ignore # Cleanup -shutil.rmtree("output", ignore_errors=True) -os.mkdir("output") +shutil.rmtree(output_dir, ignore_errors=True) +os.mkdir(output_dir) # Initialize apngasm = APNGAsm() @@ -41,20 +49,22 @@ def frame_info(frame: APNGFrame): 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_dir)): + apngasm.add_frame_from_file(os.path.join(frames_dir, 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") +out = os.path.join(output_dir, "elephant-frame.png") +frame.save(out) # Getting one frame as Pillow Image mode = color_type_dict[frame.color_type] im = Image.frombytes(mode, (frame.width, frame.height), frame.pixels) # type: ignore -im.save("output/elephant-frame-pillow.png") +out = os.path.join(output_dir, "elephant-frame-pillow.png") +im.save(out) # Get inforamtion about whole animation print(f"{apngasm.get_loops() = }") @@ -62,44 +72,47 @@ def frame_info(frame: APNGFrame): print(f"{apngasm.frame_count() = }") # Assemble -success = apngasm.assemble("output/elephant.apng") +out = os.path.join(output_dir, "elephant.png") +success = apngasm.assemble(out) 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") +frames = apngasm.disassemble(ball_apng_path) 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) # type: ignore -im.save("output/ball0.png") +out = os.path.join(output_dir, "ball0.png") +im.save(out) # Disassemble all APNG into PNGs -apngasm.save_pngs("output") +apngasm.save_pngs(output_dir) # 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_dir)) +for file_name in sorted(os.listdir(frames_dir)): + image = Image.open(os.path.join(frames_dir, 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") +out = os.path.join(output_dir, "elephant-spinning-pillow.apng") +success = apngasm.assemble(out) 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(palette_png_path, 100, 1000) +apngasm.add_frame_from_file(grey_png_path, 100, 1000) frame0 = apngasm.get_frames()[0] frame_info(frame0) @@ -107,24 +120,25 @@ def frame_info(frame: APNGFrame): frame1 = apngasm.get_frames()[1] frame_info(frame1) -success = apngasm.assemble("output/birds.apng") +out = os.path.join(output_dir, "birds.apng") +success = apngasm.assemble(out) print(f"{success = }") del apngasm # Assemble palette and grey PNGs, but with Pillow -image0 = Image.open("input/grey.png").convert("RGB") +image0 = Image.open(grey_png_path).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") +image1 = Image.open(grey_png_path).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(palette_png_path).convert("RGBA") frame2 = APNGFrame() frame2.delay_num = 1 frame2.delay_den = 1 @@ -137,5 +151,6 @@ def frame_info(frame: APNGFrame): # Another way of creating APNGAsm object apngasm = APNGAsm([frame0, frame1, frame2]) # type: ignore -success = apngasm.assemble("output/birds-pillow.apng") +out = os.path.join(output_dir, "birds-pillow.apng") +success = apngasm.assemble(out) print(f"{success = }") diff --git a/example/frames/elephant-001.png b/samples/frames/elephant-001.png similarity index 100% rename from example/frames/elephant-001.png rename to samples/frames/elephant-001.png diff --git a/example/frames/elephant-002.png b/samples/frames/elephant-002.png similarity index 100% rename from example/frames/elephant-002.png rename to samples/frames/elephant-002.png diff --git a/example/frames/elephant-003.png b/samples/frames/elephant-003.png similarity index 100% rename from example/frames/elephant-003.png rename to samples/frames/elephant-003.png diff --git a/example/frames/elephant-004.png b/samples/frames/elephant-004.png similarity index 100% rename from example/frames/elephant-004.png rename to samples/frames/elephant-004.png diff --git a/example/frames/elephant-005.png b/samples/frames/elephant-005.png similarity index 100% rename from example/frames/elephant-005.png rename to samples/frames/elephant-005.png diff --git a/example/frames/elephant-006.png b/samples/frames/elephant-006.png similarity index 100% rename from example/frames/elephant-006.png rename to samples/frames/elephant-006.png diff --git a/example/frames/elephant-007.png b/samples/frames/elephant-007.png similarity index 100% rename from example/frames/elephant-007.png rename to samples/frames/elephant-007.png diff --git a/example/frames/elephant-008.png b/samples/frames/elephant-008.png similarity index 100% rename from example/frames/elephant-008.png rename to samples/frames/elephant-008.png diff --git a/example/frames/elephant-009.png b/samples/frames/elephant-009.png similarity index 100% rename from example/frames/elephant-009.png rename to samples/frames/elephant-009.png diff --git a/example/frames/elephant-010.png b/samples/frames/elephant-010.png similarity index 100% rename from example/frames/elephant-010.png rename to samples/frames/elephant-010.png diff --git a/example/frames/elephant-011.png b/samples/frames/elephant-011.png similarity index 100% rename from example/frames/elephant-011.png rename to samples/frames/elephant-011.png diff --git a/example/frames/elephant-012.png b/samples/frames/elephant-012.png similarity index 100% rename from example/frames/elephant-012.png rename to samples/frames/elephant-012.png diff --git a/example/frames/elephant-013.png b/samples/frames/elephant-013.png similarity index 100% rename from example/frames/elephant-013.png rename to samples/frames/elephant-013.png diff --git a/example/frames/elephant-014.png b/samples/frames/elephant-014.png similarity index 100% rename from example/frames/elephant-014.png rename to samples/frames/elephant-014.png diff --git a/example/frames/elephant-015.png b/samples/frames/elephant-015.png similarity index 100% rename from example/frames/elephant-015.png rename to samples/frames/elephant-015.png diff --git a/example/frames/elephant-016.png b/samples/frames/elephant-016.png similarity index 100% rename from example/frames/elephant-016.png rename to samples/frames/elephant-016.png diff --git a/example/frames/elephant-017.png b/samples/frames/elephant-017.png similarity index 100% rename from example/frames/elephant-017.png rename to samples/frames/elephant-017.png diff --git a/example/frames/elephant-018.png b/samples/frames/elephant-018.png similarity index 100% rename from example/frames/elephant-018.png rename to samples/frames/elephant-018.png diff --git a/example/frames/elephant-019.png b/samples/frames/elephant-019.png similarity index 100% rename from example/frames/elephant-019.png rename to samples/frames/elephant-019.png diff --git a/example/frames/elephant-020.png b/samples/frames/elephant-020.png similarity index 100% rename from example/frames/elephant-020.png rename to samples/frames/elephant-020.png diff --git a/example/frames/elephant-021.png b/samples/frames/elephant-021.png similarity index 100% rename from example/frames/elephant-021.png rename to samples/frames/elephant-021.png diff --git a/example/frames/elephant-022.png b/samples/frames/elephant-022.png similarity index 100% rename from example/frames/elephant-022.png rename to samples/frames/elephant-022.png diff --git a/example/frames/elephant-023.png b/samples/frames/elephant-023.png similarity index 100% rename from example/frames/elephant-023.png rename to samples/frames/elephant-023.png diff --git a/example/frames/elephant-024.png b/samples/frames/elephant-024.png similarity index 100% rename from example/frames/elephant-024.png rename to samples/frames/elephant-024.png diff --git a/example/frames/elephant-025.png b/samples/frames/elephant-025.png similarity index 100% rename from example/frames/elephant-025.png rename to samples/frames/elephant-025.png diff --git a/example/frames/elephant-026.png b/samples/frames/elephant-026.png similarity index 100% rename from example/frames/elephant-026.png rename to samples/frames/elephant-026.png diff --git a/example/frames/elephant-027.png b/samples/frames/elephant-027.png similarity index 100% rename from example/frames/elephant-027.png rename to samples/frames/elephant-027.png diff --git a/example/frames/elephant-028.png b/samples/frames/elephant-028.png similarity index 100% rename from example/frames/elephant-028.png rename to samples/frames/elephant-028.png diff --git a/example/frames/elephant-029.png b/samples/frames/elephant-029.png similarity index 100% rename from example/frames/elephant-029.png rename to samples/frames/elephant-029.png diff --git a/example/frames/elephant-030.png b/samples/frames/elephant-030.png similarity index 100% rename from example/frames/elephant-030.png rename to samples/frames/elephant-030.png diff --git a/example/frames/elephant-031.png b/samples/frames/elephant-031.png similarity index 100% rename from example/frames/elephant-031.png rename to samples/frames/elephant-031.png diff --git a/example/frames/elephant-032.png b/samples/frames/elephant-032.png similarity index 100% rename from example/frames/elephant-032.png rename to samples/frames/elephant-032.png diff --git a/example/frames/elephant-033.png b/samples/frames/elephant-033.png similarity index 100% rename from example/frames/elephant-033.png rename to samples/frames/elephant-033.png diff --git a/example/frames/elephant-034.png b/samples/frames/elephant-034.png similarity index 100% rename from example/frames/elephant-034.png rename to samples/frames/elephant-034.png diff --git a/example/input/grey.png b/samples/input/0.png similarity index 100% rename from example/input/grey.png rename to samples/input/0.png diff --git a/example/input/palette.png b/samples/input/1.png similarity index 100% rename from example/input/palette.png rename to samples/input/1.png diff --git a/samples/input/animation_spec.json b/samples/input/animation_spec.json new file mode 100644 index 0000000..a5bdb93 --- /dev/null +++ b/samples/input/animation_spec.json @@ -0,0 +1,12 @@ +{ + "loops": "0", + "skip_first": "false", + "frames": [ + { + "0.png": "100\/1000" + }, + { + "1.png": "100\/1000" + } + ] +} diff --git a/samples/input/animation_spec.xml b/samples/input/animation_spec.xml new file mode 100644 index 0000000..79621a9 --- /dev/null +++ b/samples/input/animation_spec.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/example/input/ball.apng b/samples/input/ball.apng similarity index 100% rename from example/input/ball.apng rename to samples/input/ball.apng diff --git a/samples/input/grey.png b/samples/input/grey.png new file mode 100644 index 0000000000000000000000000000000000000000..212cea147acdafb49a730f80c0da42d295915b9c GIT binary patch literal 20418 zcmV(#K;*xPP)&hWtdSxd9(&^d zz@FF>MOZ;u_P~@JM2NE30w|!01yt3m*S~lB`|D1>o7H5d2lo}AJvwJ}a<84|+u3~i zB?15%0D`^x#3W__0Kkxsa)%#{nr5ZKRF0E05CLo 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 literal 0 HcmV?d00001 diff --git a/samples/input/palette.png b/samples/input/palette.png new file mode 100644 index 0000000000000000000000000000000000000000..b6555f632b8fdc3ca4115850016189301df7c7b0 GIT binary patch literal 17680 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 Date: Tue, 27 Feb 2024 13:51:49 +0800 Subject: [PATCH 036/103] Run pytest during check_and_fix --- .github/workflows/check_and_fix.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/check_and_fix.yml b/.github/workflows/check_and_fix.yml index 0bc9d8f..e6b3aac 100644 --- a/.github/workflows/check_and_fix.yml +++ b/.github/workflows/check_and_fix.yml @@ -36,6 +36,10 @@ jobs: -m apngasm_python._apngasm_python \ -o src-python/apngasm_python/_apngasm_python.pyi \ -M src-python/apngasm_python/py.typed + - name: Pytest + run: | + pip install pytest Pillow numpy && + pytest - name: Commit & Push changes uses: actions-js/push@master with: From c0ee3a7d2ec0f437468953956a29235e9943a277 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Tue, 27 Feb 2024 13:57:22 +0800 Subject: [PATCH 037/103] Test build --- .github/workflows/build.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 54860ca..feac540 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,9 +4,9 @@ on: push: tags: - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 - release: - types: - - [published] + release: + types: + - published jobs: build_sdist: @@ -135,6 +135,7 @@ jobs: # upload_pypi_test: # needs: [build_wheels, build_sdist] # runs-on: ubuntu-latest + # if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') # steps: # - uses: actions/download-artifact@v3 # with: @@ -152,6 +153,7 @@ jobs: upload_pypi: needs: [build_wheels, build_sdist] runs-on: ubuntu-latest + if: github.event_name == 'release' && github.event.action == 'published' steps: - uses: actions/download-artifact@v3 with: From 9158cd9ea3a4ab75b4c37427f65c278a8c9955f1 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Tue, 27 Feb 2024 14:36:20 +0800 Subject: [PATCH 038/103] Fix build --- .github/workflows/build.yml | 9 +++++++-- tests/test_binder.py | 9 ++++++++- tests/test_direct.py | 26 ++++++++++++++++++++++---- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index feac540..3d7e6c8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,12 +11,16 @@ on: jobs: build_sdist: name: Build source distribution - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 with: submodules: recursive + - uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: Build sdist run: pipx run build --sdist @@ -28,7 +32,7 @@ jobs: - uses: actions/upload-artifact@v3 with: - path: dist/*.tar.gz + path: dist/apngasm_python-*.tar.gz build_wheels: name: Build wheels for ${{ matrix.os }} ${{ matrix.cibw_archs }} ${{ matrix.cibw_build }} @@ -127,6 +131,7 @@ jobs: CIBW_BEFORE_TEST: pip install --only-binary ":all:" Pillow numpy; true CIBW_BEFORE_TEST_WINDOWS: pip install --only-binary ":all:" Pillow numpy || VER>NUL CIBW_TEST_COMMAND: pytest {package}/tests + CIBW_TEST_SKIP: pp* - uses: actions/upload-artifact@v3 with: diff --git a/tests/test_binder.py b/tests/test_binder.py index df48fb5..6329084 100644 --- a/tests/test_binder.py +++ b/tests/test_binder.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import os +import sys import pytest @@ -48,7 +49,7 @@ def test_frame_pixels_as_pillow(): @pytest.mark.skipif(NUMPY_LOADED is False, reason="Numpy not installed") -def test_frame_pixels_as_pillow(): +def test_frame_pixels_as_numpy(): import numpy apngasm = APNGAsmBinder() @@ -313,6 +314,9 @@ def test_load_animation_spec_json(): assert len(frames) == 2 +@pytest.mark.skipif( + sys.platform == "win32", reason="Bug in apngasm causing incorrect path" +) def test_save_json(tmpdir): apngasm = APNGAsmBinder() apngasm.add_frame_from_file(animation_spec_0_png_path) @@ -325,6 +329,9 @@ def test_save_json(tmpdir): assert f.read() == g.read() +@pytest.mark.skipif( + sys.platform == "win32", reason="Bug in apngasm causing incorrect path" +) def test_save_xml(tmpdir): apngasm = APNGAsmBinder() apngasm.add_frame_from_file(animation_spec_0_png_path) diff --git a/tests/test_direct.py b/tests/test_direct.py index 5029ebc..34c89db 100644 --- a/tests/test_direct.py +++ b/tests/test_direct.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import os +import sys import pytest @@ -62,6 +63,7 @@ def test_frame_width(): assert frame.width == 480 + @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") @pytest.mark.skipif(NUMPY_LOADED is False, reason="Numpy not installed") def test_frame_height(): @@ -73,6 +75,7 @@ def test_frame_height(): assert frame.height == 400 + @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") @pytest.mark.skipif(NUMPY_LOADED is False, reason="Numpy not installed") def test_frame_color_type(): @@ -103,10 +106,13 @@ def test_frame_transparency(): import numpy image = Image.open(elephant_frame0_path).convert("RGB") - frame = create_frame_from_rgb_trns(numpy.array(image), image.width, image.height, numpy.array([255, 255, 255])) + frame = create_frame_from_rgb_trns( + numpy.array(image), image.width, image.height, numpy.array([255, 255, 255]) + ) assert isinstance(frame.transparency, numpy.ndarray) + @pytest.mark.skipif(NUMPY_LOADED is False, reason="Numpy not installed") @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") def test_frame_palette_size(): @@ -126,7 +132,9 @@ def test_frame_transparency_size(): import numpy image = Image.open(elephant_frame0_path).convert("RGB") - frame = create_frame_from_rgb_trns(numpy.array(image), image.width, image.height, numpy.array([255, 255, 255])) + frame = create_frame_from_rgb_trns( + numpy.array(image), image.width, image.height, numpy.array([255, 255, 255]) + ) assert frame.transparency_size == 6 @@ -137,7 +145,9 @@ def test_frame_delay_num(): import numpy image = Image.open(elephant_frame0_path) - frame = create_frame_from_rgba(numpy.array(image), image.width, image.height, 50, 250) + frame = create_frame_from_rgba( + numpy.array(image), image.width, image.height, 50, 250 + ) assert frame.delay_num == 50 @@ -148,7 +158,9 @@ def test_frame_delay_den(): import numpy image = Image.open(elephant_frame0_path) - frame = create_frame_from_rgba(numpy.array(image), image.width, image.height, 50, 250) + frame = create_frame_from_rgba( + numpy.array(image), image.width, image.height, 50, 250 + ) assert frame.delay_den == 250 @@ -212,6 +224,9 @@ def test_load_animation_spec_json(): assert len(frames) == 2 +@pytest.mark.skipif( + sys.platform == "win32", reason="Bug in apngasm causing incorrect path" +) def test_save_json(tmpdir): apngasm = APNGAsm() apngasm.add_frame_from_file(animation_spec_0_png_path) @@ -224,6 +239,9 @@ def test_save_json(tmpdir): assert f.read() == g.read() +@pytest.mark.skipif( + sys.platform == "win32", reason="Bug in apngasm causing incorrect path" +) def test_save_xml(tmpdir): apngasm = APNGAsm() apngasm.add_frame_from_file(animation_spec_0_png_path) From b077ef21d987aee162cc5b1592393f6a83889f3f Mon Sep 17 00:00:00 2001 From: laggykiller Date: Tue, 27 Feb 2024 14:47:51 +0800 Subject: [PATCH 039/103] Add test for with statement --- tests/test_binder.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_binder.py b/tests/test_binder.py index 6329084..89527d0 100644 --- a/tests/test_binder.py +++ b/tests/test_binder.py @@ -400,3 +400,7 @@ def test_reset(): def test_version(): apngasm = APNGAsmBinder() assert isinstance(apngasm.version(), str) + +def test_with(): + with APNGAsmBinder() as apngasm: + assert isinstance(apngasm.version(), str) \ No newline at end of file From 3b7f5105166b9f81e13b6295f22ab345bd327154 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Tue, 27 Feb 2024 15:55:47 +0800 Subject: [PATCH 040/103] Change macOS version for runner --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3d7e6c8..64c28c7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,21 +53,21 @@ jobs: cibw_archs: ARM64 cibw_build: "*" cibw_environment: APNGASM_COMPILE_TARGET=armv8 - - os: macos-11 + - os: macos-12 cibw_archs: x86_64 cibw_build: "*" cibw_environment: > APNGASM_COMPILE_TARGET=x86_64 _PYTHON_HOST_PLATFORM=macosx-10.15-x86_64 MACOSX_DEPLOYMENT_TARGET=10.15 - - os: macos-11 + - os: macos-14 cibw_archs: arm64 cibw_build: "*" cibw_environment: > APNGASM_COMPILE_TARGET=armv8 _PYTHON_HOST_PLATFORM=macosx-11.0-arm64 MACOSX_DEPLOYMENT_TARGET=11.0 - - os: macos-11 + - os: macos-14 cibw_archs: universal2 cibw_build: "*" cibw_environment: > From c046caa64b633fe1cc957f67fd4fbc0f6abd7b40 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Tue, 27 Feb 2024 16:26:58 +0800 Subject: [PATCH 041/103] Use last good nanobind for build testing --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3af9a03..6ce304b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ Tracker = "https://github.com/laggykiller/apngasm-python/issues" [build-system] # How pip and other frontends should build this project requires = [ "py-build-cmake==0.2.0a7", - "nanobind @ git+https://github.com/wjakob/nanobind.git", + "nanobind @ git+https://github.com/wjakob/nanobind.git@79a6a9f", "conan>=2.0", "wheel" ] From 22274a22f188c04d1beea8941f756ba1598a9f85 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Tue, 27 Feb 2024 16:43:54 +0800 Subject: [PATCH 042/103] Try building with py-build-cmake==0.2.0.a12 --- .github/workflows/build.yml | 1 - pyproject.toml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 64c28c7..ee3fe64 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -123,7 +123,6 @@ jobs: env: CIBW_BUILD_FRONTEND: build CIBW_ARCHS: ${{ matrix.cibw_archs }} - CIBW_BEFORE_ALL: ${{ matrix.cibw_before_all }} # CIBW_ENVIRONMENT: PY_BUILD_CMAKE_VERBOSE=1 ${{ matrix.cibw_environment }} CIBW_ENVIRONMENT: ${{ matrix.cibw_environment }} CIBW_BUILD: ${{ matrix.cibw_build }} diff --git a/pyproject.toml b/pyproject.toml index 6ce304b..f8cc261 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ Tracker = "https://github.com/laggykiller/apngasm-python/issues" [build-system] # How pip and other frontends should build this project requires = [ - "py-build-cmake==0.2.0a7", + "py-build-cmake==0.2.0a12", "nanobind @ git+https://github.com/wjakob/nanobind.git@79a6a9f", "conan>=2.0", "wheel" From 5266a9002b2c3ead59322f849aade46e93be9af2 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Tue, 27 Feb 2024 17:04:04 +0800 Subject: [PATCH 043/103] use macos-12 and disable s390x as apngasm not working there --- .github/workflows/build.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ee3fe64..12addb8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,14 +60,14 @@ jobs: APNGASM_COMPILE_TARGET=x86_64 _PYTHON_HOST_PLATFORM=macosx-10.15-x86_64 MACOSX_DEPLOYMENT_TARGET=10.15 - - os: macos-14 + - os: macos-12 cibw_archs: arm64 cibw_build: "*" cibw_environment: > APNGASM_COMPILE_TARGET=armv8 _PYTHON_HOST_PLATFORM=macosx-11.0-arm64 MACOSX_DEPLOYMENT_TARGET=11.0 - - os: macos-14 + - os: macos-12 cibw_archs: universal2 cibw_build: "*" cibw_environment: > @@ -98,14 +98,14 @@ jobs: cibw_archs: ppc64le cibw_build: "*-manylinux_*" cibw_environment: APNGASM_COMPILE_TARGET=ppc64le - - os: ubuntu-20.04 - cibw_archs: s390x - cibw_build: "*-musllinux_*" - cibw_environment: APNGASM_COMPILE_TARGET=s390x - - os: ubuntu-20.04 - cibw_archs: s390x - cibw_build: "*-manylinux_*" - cibw_environment: APNGASM_COMPILE_TARGET=s390x + # - os: ubuntu-20.04 + # cibw_archs: s390x + # cibw_build: "*-musllinux_*" + # cibw_environment: APNGASM_COMPILE_TARGET=s390x + # - os: ubuntu-20.04 + # cibw_archs: s390x + # cibw_build: "*-manylinux_*" + # cibw_environment: APNGASM_COMPILE_TARGET=s390x steps: - uses: actions/checkout@v3 From 5d47d3d445975c3435b327568cc1af994d01b616 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Tue, 27 Feb 2024 17:29:52 +0800 Subject: [PATCH 044/103] Skip testing on universal2 --- .github/workflows/build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 12addb8..8c0f603 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -130,7 +130,9 @@ jobs: CIBW_BEFORE_TEST: pip install --only-binary ":all:" Pillow numpy; true CIBW_BEFORE_TEST_WINDOWS: pip install --only-binary ":all:" Pillow numpy || VER>NUL CIBW_TEST_COMMAND: pytest {package}/tests - CIBW_TEST_SKIP: pp* + # Skip test for universal2 wheels as it could not be installed during CI + # yet it could be installed on local machine during test + CIBW_TEST_SKIP: "pp* *-macosx_universal2" - uses: actions/upload-artifact@v3 with: From 7f6e94970840d371e0a4f2e114f0c37c66920cd3 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Tue, 27 Feb 2024 17:32:51 +0800 Subject: [PATCH 045/103] Fix repeated function name in test --- tests/test_binder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_binder.py b/tests/test_binder.py index 89527d0..5b2e1c2 100644 --- a/tests/test_binder.py +++ b/tests/test_binder.py @@ -214,7 +214,7 @@ def test_add_frame_from_numpy_rgba(): @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") @pytest.mark.skipif(NUMPY_LOADED is False, reason="Numpy not installed") -def test_add_frame_from_numpy_rgb_trns(): +def test_add_frame_from_numpy_rgb(): from PIL import Image import numpy From b47196d8354a9b44137bb78005cdabd2679c27a3 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Tue, 27 Feb 2024 17:33:02 +0800 Subject: [PATCH 046/103] Enable pyright in check_and_fix --- .github/workflows/check_and_fix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check_and_fix.yml b/.github/workflows/check_and_fix.yml index e6b3aac..9cc753d 100644 --- a/.github/workflows/check_and_fix.yml +++ b/.github/workflows/check_and_fix.yml @@ -28,7 +28,7 @@ jobs: pip install git+https://github.com/wjakob/nanobind.git ruff check src-python ruff check scripts - # pyright + pyright pip install . ruff format src-python ruff format scripts From 2f96dbc6ad9617bdd4fd686404c007e957aeab9f Mon Sep 17 00:00:00 2001 From: laggykiller Date: Tue, 27 Feb 2024 17:35:43 +0800 Subject: [PATCH 047/103] Install pillow and numpy during check_and_fix --- .github/workflows/check_and_fix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check_and_fix.yml b/.github/workflows/check_and_fix.yml index 9cc753d..e0f4e30 100644 --- a/.github/workflows/check_and_fix.yml +++ b/.github/workflows/check_and_fix.yml @@ -24,7 +24,7 @@ jobs: python-version: '3.8' - name: Check, update stub and formatting run: | - pip install build ruff pyright + pip install build ruff pyright Pillow numpy pip install git+https://github.com/wjakob/nanobind.git ruff check src-python ruff check scripts From 033c91390ebf6f07546301f8003b0815cac9c608 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Tue, 27 Feb 2024 17:36:57 +0800 Subject: [PATCH 048/103] Invoke pyright after installing in check_and_fix --- .github/workflows/check_and_fix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check_and_fix.yml b/.github/workflows/check_and_fix.yml index e0f4e30..6e708ed 100644 --- a/.github/workflows/check_and_fix.yml +++ b/.github/workflows/check_and_fix.yml @@ -28,8 +28,8 @@ jobs: pip install git+https://github.com/wjakob/nanobind.git ruff check src-python ruff check scripts - pyright pip install . + pyright ruff format src-python ruff format scripts python -m nanobind.stubgen \ From 0194e6f33b4e6cef6cb29bfbfc7b6fa1cc355030 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Tue, 27 Feb 2024 17:43:06 +0800 Subject: [PATCH 049/103] Fix check_and_fix --- .github/workflows/check_and_fix.yml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/check_and_fix.yml b/.github/workflows/check_and_fix.yml index 6e708ed..68405ea 100644 --- a/.github/workflows/check_and_fix.yml +++ b/.github/workflows/check_and_fix.yml @@ -22,24 +22,30 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.8' - - name: Check, update stub and formatting + - name: Install dependency run: | - pip install build ruff pyright Pillow numpy + pip install build ruff pyright Pillow numpy pytest pip install git+https://github.com/wjakob/nanobind.git + - name: Ruff check + run: | ruff check src-python ruff check scripts - pip install . - pyright + - name: Install test + run: pip install . + - name: pyright + run: pyright + - name: ruff format + run: | ruff format src-python ruff format scripts + - name: nanobind stubgen + run: | python -m nanobind.stubgen \ -m apngasm_python._apngasm_python \ -o src-python/apngasm_python/_apngasm_python.pyi \ -M src-python/apngasm_python/py.typed - name: Pytest - run: | - pip install pytest Pillow numpy && - pytest + run: pytest - name: Commit & Push changes uses: actions-js/push@master with: From e05abc7eddc8969943dfeabbd18a8c403561944d Mon Sep 17 00:00:00 2001 From: laggykiller Date: Tue, 27 Feb 2024 18:21:17 +0800 Subject: [PATCH 050/103] Use nanobind master branch again --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f8cc261..aaa16ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ Tracker = "https://github.com/laggykiller/apngasm-python/issues" [build-system] # How pip and other frontends should build this project requires = [ "py-build-cmake==0.2.0a12", - "nanobind @ git+https://github.com/wjakob/nanobind.git@79a6a9f", + "nanobind @ git+https://github.com/wjakob/nanobind.git", "conan>=2.0", "wheel" ] From e0c878e3725904c3ba8a0e0ffad55849a60e8011 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Wed, 28 Feb 2024 09:48:58 +0800 Subject: [PATCH 051/103] Do not specify generator --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index aaa16ed..3a48dd1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,12 +55,10 @@ python_abi = 'abi3' abi3_minimum_cpython_version = 312 [tool.py-build-cmake.linux.cmake] # Linux-specific options -generator = "Ninja Multi-Config" config = "Release" env = { "CMAKE_PREFIX_PATH" = "${HOME}/.local" } [tool.py-build-cmake.mac.cmake] # macOS-specific options -generator = "Ninja Multi-Config" config = "Release" [tool.py-build-cmake.windows.cmake] # Windows-specific options From 677aaeca969669c738bbf00a037d0271fa263d05 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Wed, 28 Feb 2024 11:18:37 +0800 Subject: [PATCH 052/103] Run conan again for each build; Build with newer macOS runner --- .github/workflows/build.yml | 4 ++-- scripts/get_deps.py | 12 ++---------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8c0f603..ae64a53 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,14 +60,14 @@ jobs: APNGASM_COMPILE_TARGET=x86_64 _PYTHON_HOST_PLATFORM=macosx-10.15-x86_64 MACOSX_DEPLOYMENT_TARGET=10.15 - - os: macos-12 + - os: macos-14 cibw_archs: arm64 cibw_build: "*" cibw_environment: > APNGASM_COMPILE_TARGET=armv8 _PYTHON_HOST_PLATFORM=macosx-11.0-arm64 MACOSX_DEPLOYMENT_TARGET=11.0 - - os: macos-12 + - os: macos-14 cibw_archs: universal2 cibw_build: "*" cibw_environment: > diff --git a/scripts/get_deps.py b/scripts/get_deps.py index 969b86b..d9a6d9c 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -83,7 +83,7 @@ def install_deps(arch: str): print("build: " + str(build)) print("options: " + str(options)) - subprocess.run(["conan", "profile", "detect"]) + subprocess.run(["conan", "profile", "detect", "-f"]) conan_output = os.path.join("conan_output", arch) @@ -106,17 +106,9 @@ def install_deps(arch: str): def main(): arch = get_arch() - if arch == "universal2": - conan_output = "conan_output/x86_64" - else: - conan_output = "conan_output/" + arch - if os.path.isdir(conan_output): - print("Dependencies found at:" + conan_output) - print("Skip conan install...") - return if arch != "universal2": - conan_output = install_deps(arch) + install_deps(arch) else: # Repeat to install the other architecture version of libwebp conan_output_x64 = install_deps("x86_64") From 3dd21f5d88fa35682133d63105db5deaa0c36d2c Mon Sep 17 00:00:00 2001 From: laggykiller Date: Wed, 28 Feb 2024 11:32:11 +0800 Subject: [PATCH 053/103] use latest cibuildwheel --- .github/workflows/build.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ae64a53..c24635b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -119,7 +119,7 @@ jobs: platforms: all - name: Build wheels for ${{ matrix.os }} ${{ matrix.cibw_archs }} ${{ matrix.cibw_build }} - uses: pypa/cibuildwheel@v2.16.5 + uses: pypa/cibuildwheel@edd67e0 env: CIBW_BUILD_FRONTEND: build CIBW_ARCHS: ${{ matrix.cibw_archs }} @@ -130,9 +130,7 @@ jobs: CIBW_BEFORE_TEST: pip install --only-binary ":all:" Pillow numpy; true CIBW_BEFORE_TEST_WINDOWS: pip install --only-binary ":all:" Pillow numpy || VER>NUL CIBW_TEST_COMMAND: pytest {package}/tests - # Skip test for universal2 wheels as it could not be installed during CI - # yet it could be installed on local machine during test - CIBW_TEST_SKIP: "pp* *-macosx_universal2" + CIBW_TEST_SKIP: pp* - uses: actions/upload-artifact@v3 with: From 5558780fa167620e3e8a9ad90908f4df5b6f7413 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Wed, 28 Feb 2024 11:34:20 +0800 Subject: [PATCH 054/103] use latest cibuildwheel --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c24635b..13f8215 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -119,7 +119,7 @@ jobs: platforms: all - name: Build wheels for ${{ matrix.os }} ${{ matrix.cibw_archs }} ${{ matrix.cibw_build }} - uses: pypa/cibuildwheel@edd67e0 + uses: pypa/cibuildwheel@edd67e0d266e14537bbf2619e8c6a379ecb038fc env: CIBW_BUILD_FRONTEND: build CIBW_ARCHS: ${{ matrix.cibw_archs }} From a371dd272f38ac99a3ba5eb4bdf5ac5f20356920 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Wed, 28 Feb 2024 12:45:57 +0800 Subject: [PATCH 055/103] Change deployment target for universal2 wheel --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 13f8215..d93a74a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,8 +72,8 @@ jobs: cibw_build: "*" cibw_environment: > APNGASM_COMPILE_TARGET=universal2 - _PYTHON_HOST_PLATFORM=macosx-11.0-universal2 - MACOSX_DEPLOYMENT_TARGET=11.0 + _PYTHON_HOST_PLATFORM=macosx-10.15-universal2 + MACOSX_DEPLOYMENT_TARGET=10.15 - os: ubuntu-20.04 cibw_archs: x86_64 cibw_build: "*" From cccf9963e8dcaff4b0711deaab5b96c48bccffc4 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Wed, 28 Feb 2024 13:34:57 +0800 Subject: [PATCH 056/103] Fix build --- CMakeLists.txt | 2 +- scripts/get_deps.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c6e6e20..7366cca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,7 +29,7 @@ if (WIN32) elseif (LINUX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") elseif (APPLE) - if(${APNGASM_COMPILE_TARGET} STREQUAL "x86_64") + if(${APNGASM_COMPILE_TARGET} STREQUAL "armv8") set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0") else() set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15") diff --git a/scripts/get_deps.py b/scripts/get_deps.py index d9a6d9c..eb9488d 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -18,10 +18,10 @@ def install_deps(arch: str): settings.append("compiler.runtime=static") elif platform.system() == "Darwin": settings.append("os=Macos") - if arch == "x86_64": - settings.append("os.version=10.15") - else: + if arch == "armv8": settings.append("os.version=11.0") + else: + settings.append("os.version=10.15") settings.append("compiler=apple-clang") settings.append("compiler.libcxx=libc++") elif platform.system() == "Linux": From a26537d519ed7358ea5e995a4a29148c21717498 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Wed, 28 Feb 2024 14:19:52 +0800 Subject: [PATCH 057/103] Fix universal2 build --- CMakeLists.txt | 5 +++-- scripts/get_arch.py | 28 ++++++++++++++++++---------- scripts/get_deps.py | 26 ++++++++++++++++++-------- 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7366cca..f56eb85 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,9 +45,10 @@ if (WIN32) endif() # Add conan packages -if ("${APNGASM_COMPILE_TARGET}" STREQUAL "universal2") - # x86_64 Contians static libraries that are universal2 +if ("${APNGASM_COMPILE_TARGET}" STREQUAL "universal2_x86_64") set(CONAN_TOOLCHAIN ${CMAKE_SOURCE_DIR}/conan_output/x86_64/conan_toolchain.cmake) +elseif ("${APNGASM_COMPILE_TARGET}" STREQUAL "universal2_armv8") + set(CONAN_TOOLCHAIN ${CMAKE_SOURCE_DIR}/conan_output/armv8/conan_toolchain.cmake) else() set(CONAN_TOOLCHAIN ${CMAKE_SOURCE_DIR}/conan_output/${APNGASM_COMPILE_TARGET}/conan_toolchain.cmake) endif() diff --git a/scripts/get_arch.py b/scripts/get_arch.py index 8c7a782..56cfbc9 100755 --- a/scripts/get_arch.py +++ b/scripts/get_arch.py @@ -12,18 +12,26 @@ } -def get_arch(): - arch = None - if os.getenv("APNGASM_COMPILE_TARGET"): - arch = os.getenv("APNGASM_COMPILE_TARGET") +def get_native_arch() -> str: + for k, v in conan_archs.items(): + if platform.machine().lower() in v: + return k + + # Failover + return platform.machine().lower() + + +def get_arch() -> str: + arch_env = os.getenv("APNGASM_COMPILE_TARGET") + if isinstance(arch_env, str): + arch = arch_env else: - for k, v in conan_archs.items(): - if platform.machine().lower() in v: - arch = k - break + arch = get_native_arch() + + if arch == "universal2": + arch = "universal2_" + get_native_arch() - if arch is None: - arch = platform.machine().lower() + assert arch in ("universal2_x86_64", "universal2_armv8") return arch diff --git a/scripts/get_deps.py b/scripts/get_deps.py index eb9488d..3e92d5a 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -107,26 +107,36 @@ def install_deps(arch: str): def main(): arch = get_arch() - if arch != "universal2": + if not arch.startswith("universal2"): 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") - shutil.rmtree(conan_output_universal2, ignore_errors=True) + + if arch.endswith("x86_64"): + lipo_dir_merge_src = conan_output_x64 + lipo_dir_merge_dst = conan_output_arm + elif arch.endswith("armv8"): + lipo_dir_merge_src = conan_output_arm + lipo_dir_merge_dst = conan_output_x64 + else: + raise RuntimeError("Invalid arch: " + arch) + + lipo_dir_merge_result = lipo_dir_merge_src.replace("armv8", "universal2") + shutil.rmtree(lipo_dir_merge_result, ignore_errors=True) subprocess.run( [ "python3", "lipo-dir-merge/lipo-dir-merge.py", - conan_output_x64, - conan_output_arm, - conan_output_universal2, + lipo_dir_merge_src, + lipo_dir_merge_dst, + lipo_dir_merge_result, ] ) - shutil.rmtree(conan_output_x64) - shutil.move(conan_output_universal2, conan_output_x64) + shutil.rmtree(lipo_dir_merge_src) + shutil.move(lipo_dir_merge_result, lipo_dir_merge_src) if __name__ == "__main__": From 751e9885a53029469e74b14b0f60260225af9034 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 28 Feb 2024 06:23:13 +0000 Subject: [PATCH 058/103] Update stub and formatting --- scripts/get_arch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/get_arch.py b/scripts/get_arch.py index 56cfbc9..70d79d4 100755 --- a/scripts/get_arch.py +++ b/scripts/get_arch.py @@ -27,7 +27,7 @@ def get_arch() -> str: arch = arch_env else: arch = get_native_arch() - + if arch == "universal2": arch = "universal2_" + get_native_arch() From 281c2629241c36975582ee12b80e15306692e5e4 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Wed, 28 Feb 2024 14:46:55 +0800 Subject: [PATCH 059/103] Fix universal2 build --- scripts/get_deps.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/get_deps.py b/scripts/get_deps.py index 3e92d5a..560462d 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -122,9 +122,10 @@ def main(): lipo_dir_merge_dst = conan_output_x64 else: raise RuntimeError("Invalid arch: " + arch) - - lipo_dir_merge_result = lipo_dir_merge_src.replace("armv8", "universal2") + + lipo_dir_merge_result = conan_output_arm.replace("armv8", "universal2") shutil.rmtree(lipo_dir_merge_result, ignore_errors=True) + subprocess.run( [ "python3", From 7458baced7bc5a8545aecdcd42771de194bee466 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 28 Feb 2024 06:49:59 +0000 Subject: [PATCH 060/103] Update stub and formatting --- scripts/get_deps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/get_deps.py b/scripts/get_deps.py index 560462d..841a7c2 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -122,10 +122,10 @@ def main(): lipo_dir_merge_dst = conan_output_x64 else: raise RuntimeError("Invalid arch: " + arch) - + lipo_dir_merge_result = conan_output_arm.replace("armv8", "universal2") shutil.rmtree(lipo_dir_merge_result, ignore_errors=True) - + subprocess.run( [ "python3", From 43fdd45e769bf149af10ae2f2a5879a16bcef09a Mon Sep 17 00:00:00 2001 From: laggykiller Date: Wed, 28 Feb 2024 15:06:55 +0800 Subject: [PATCH 061/103] Fix universal2 build --- scripts/get_arch.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/scripts/get_arch.py b/scripts/get_arch.py index 70d79d4..116f455 100755 --- a/scripts/get_arch.py +++ b/scripts/get_arch.py @@ -38,11 +38,7 @@ def get_arch() -> str: def main(): arch = get_arch() - compile_target = os.getenv("APNGASM_COMPILE_TARGET") - if compile_target: - sys.stdout.write(compile_target) - else: - sys.stdout.write(arch) + sys.stdout.write(arch) if __name__ == "__main__": From 95b9a81f554c2980fde4e6997c78819676160164 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Wed, 28 Feb 2024 15:33:03 +0800 Subject: [PATCH 062/103] Clean conan_output before build --- scripts/get_deps.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/get_deps.py b/scripts/get_deps.py index 841a7c2..ebb2c03 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -86,6 +86,7 @@ def install_deps(arch: str): subprocess.run(["conan", "profile", "detect", "-f"]) conan_output = os.path.join("conan_output", arch) + shutil.rmtree(conan_output, ignore_errors=True) subprocess.run( [ From 4c072d9ec03b19f5d1e813e50d9cc06d00ed113a Mon Sep 17 00:00:00 2001 From: laggykiller Date: Wed, 28 Feb 2024 18:08:48 +0800 Subject: [PATCH 063/103] Patching conan toolchain for universal2 --- scripts/get_deps.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/scripts/get_deps.py b/scripts/get_deps.py index ebb2c03..500bfa5 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -105,6 +105,21 @@ def install_deps(arch: str): return conan_output +def patch_conan_toolchain_universal2(lipo_dir_merge_src: str): + conan_toolchain_path = os.path.join(lipo_dir_merge_src, "conan_toolchain.cmake") + + result = "" + with open(conan_toolchain_path) as f: + for line in f: + if line.startswith("set(CMAKE_OSX_ARCHITECTURES"): + result += "# " + line + else: + result += line + + with open(conan_toolchain_path, "w_") as f: + f.write(result) + + def main(): arch = get_arch() @@ -140,6 +155,8 @@ def main(): shutil.rmtree(lipo_dir_merge_src) shutil.move(lipo_dir_merge_result, lipo_dir_merge_src) + patch_conan_toolchain_universal2(lipo_dir_merge_src) + if __name__ == "__main__": main() From c6636d4bd21646d1d4e532a6ac4cbf700f238d09 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 28 Feb 2024 10:13:00 +0000 Subject: [PATCH 064/103] Update stub and formatting --- scripts/get_deps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/get_deps.py b/scripts/get_deps.py index 500bfa5..8864214 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -115,7 +115,7 @@ def patch_conan_toolchain_universal2(lipo_dir_merge_src: str): result += "# " + line else: result += line - + with open(conan_toolchain_path, "w_") as f: f.write(result) From 994846e8a49c0ed33c459d983c401a10fa54aa1b Mon Sep 17 00:00:00 2001 From: laggykiller Date: Wed, 28 Feb 2024 18:15:00 +0800 Subject: [PATCH 065/103] Also format example and tests --- .github/workflows/check_and_fix.yml | 2 ++ example/example_binder.py | 2 +- example/example_direct.py | 11 ++++++----- tests/test_binder.py | 3 ++- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/check_and_fix.yml b/.github/workflows/check_and_fix.yml index 68405ea..c2f7dcc 100644 --- a/.github/workflows/check_and_fix.yml +++ b/.github/workflows/check_and_fix.yml @@ -38,6 +38,8 @@ jobs: run: | ruff format src-python ruff format scripts + ruff format tests + ruff format example - name: nanobind stubgen run: | python -m nanobind.stubgen \ diff --git a/example/example_binder.py b/example/example_binder.py index 59ba5d1..79bc6f3 100755 --- a/example/example_binder.py +++ b/example/example_binder.py @@ -38,7 +38,7 @@ # Getting one frame as Pillow Image im = apngasm.frame_pixels_as_pillow(0) out = os.path.join(output_dir, "elephant-frame-pillow.png") -im.save(out) # type: ignore +im.save(out) # type: ignore # Get inforamtion about whole animation print(f"{apngasm.get_loops() = }") diff --git a/example/example_direct.py b/example/example_direct.py index 3fda2db..979a61a 100755 --- a/example/example_direct.py +++ b/example/example_direct.py @@ -20,6 +20,7 @@ grey_png_path = os.path.join(input_dir, "grey.png") palette_png_path = os.path.join(input_dir, "palette.png") + def frame_info(frame: APNGFrame): print(f"{frame.pixels = }") print(f"{frame.width = }") @@ -36,7 +37,7 @@ def frame_info(frame: APNGFrame): # https://www.w3.org/TR/PNG-Chunks.html 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())) # type: ignore +color_type_dict.update(dict((v, k) for k, v in color_type_dict.items())) # type: ignore # Cleanup shutil.rmtree(output_dir, ignore_errors=True) @@ -62,7 +63,7 @@ def frame_info(frame: APNGFrame): # Getting one frame as Pillow Image mode = color_type_dict[frame.color_type] -im = Image.frombytes(mode, (frame.width, frame.height), frame.pixels) # type: ignore +im = Image.frombytes(mode, (frame.width, frame.height), frame.pixels) # type: ignore out = os.path.join(output_dir, "elephant-frame-pillow.png") im.save(out) @@ -85,7 +86,7 @@ def frame_info(frame: APNGFrame): frame = frames[0] frame_info(frame) mode = color_type_dict[frame.color_type] -im = Image.frombytes(mode, (frame.width, frame.height), frame.pixels) # type: ignore +im = Image.frombytes(mode, (frame.width, frame.height), frame.pixels) # type: ignore out = os.path.join(output_dir, "ball0.png") im.save(out) @@ -142,14 +143,14 @@ def frame_info(frame: APNGFrame): frame2 = APNGFrame() frame2.delay_num = 1 frame2.delay_den = 1 -frame2.color_type = color_type_dict[image2.mode] # type: ignore +frame2.color_type = color_type_dict[image2.mode] # type: ignore frame2.width = image2.width frame2.height = image2.height frame2.pixels = np.array(image2) frame_info(frame2) # Another way of creating APNGAsm object -apngasm = APNGAsm([frame0, frame1, frame2]) # type: ignore +apngasm = APNGAsm([frame0, frame1, frame2]) # type: ignore out = os.path.join(output_dir, "birds-pillow.apng") success = apngasm.assemble(out) diff --git a/tests/test_binder.py b/tests/test_binder.py index 5b2e1c2..9fb38fb 100644 --- a/tests/test_binder.py +++ b/tests/test_binder.py @@ -401,6 +401,7 @@ def test_version(): apngasm = APNGAsmBinder() assert isinstance(apngasm.version(), str) + def test_with(): with APNGAsmBinder() as apngasm: - assert isinstance(apngasm.version(), str) \ No newline at end of file + assert isinstance(apngasm.version(), str) From 54eb1ef79e5eacaca8a2837b20aa2e9f11e13869 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Wed, 28 Feb 2024 18:21:28 +0800 Subject: [PATCH 066/103] Linting --- .github/workflows/check_and_fix.yml | 2 ++ tests/test_binder.py | 22 ++++++---------------- tests/test_direct.py | 22 ++++++---------------- 3 files changed, 14 insertions(+), 32 deletions(-) diff --git a/.github/workflows/check_and_fix.yml b/.github/workflows/check_and_fix.yml index c2f7dcc..85bd2be 100644 --- a/.github/workflows/check_and_fix.yml +++ b/.github/workflows/check_and_fix.yml @@ -30,6 +30,8 @@ jobs: run: | ruff check src-python ruff check scripts + ruff check tests + ruff check example - name: Install test run: pip install . - name: pyright diff --git a/tests/test_binder.py b/tests/test_binder.py index 9fb38fb..be7988e 100644 --- a/tests/test_binder.py +++ b/tests/test_binder.py @@ -1,26 +1,16 @@ #!/usr/bin/env python3 +from importlib.util import find_spec import os import sys import pytest -try: - from PIL import Image - - PILLOW_LOADED = True -except ModuleNotFoundError: - PILLOW_LOADED = False - -try: - import numpy - - NUMPY_LOADED = True -except ModuleNotFoundError: - NUMPY_LOADED = False - from apngasm_python.apngasm import APNGAsmBinder from apngasm_python._apngasm_python import APNGFrame +PILLOW_LOADED = True if find_spec("PIL") else False +NUMPY_LOADED = True if find_spec("numpy") else False + file_dir = os.path.split(__file__)[0] samples_dir = os.path.join(file_dir, "../samples") frames_dir = os.path.join(samples_dir, "frames") @@ -361,9 +351,9 @@ def test_skip_first(): apngasm = APNGAsmBinder() apngasm.add_frame_from_file(elephant_frame0_path) apngasm.set_skip_first(True) - assert apngasm.is_skip_first() == True + assert apngasm.is_skip_first() is True apngasm.set_skip_first(False) - assert apngasm.is_skip_first() == False + assert apngasm.is_skip_first() is False def test_get_frames(): diff --git a/tests/test_direct.py b/tests/test_direct.py index 34c89db..fc6ca9b 100644 --- a/tests/test_direct.py +++ b/tests/test_direct.py @@ -1,23 +1,10 @@ #!/usr/bin/env python3 +from importlib.util import find_spec import os import sys import pytest -try: - from PIL import Image - - PILLOW_LOADED = True -except ModuleNotFoundError: - PILLOW_LOADED = False - -try: - import numpy - - NUMPY_LOADED = True -except ModuleNotFoundError: - NUMPY_LOADED = False - from apngasm_python._apngasm_python import ( APNGAsm, APNGFrame, @@ -25,6 +12,9 @@ create_frame_from_rgba, ) +PILLOW_LOADED = True if find_spec("PIL") else False +NUMPY_LOADED = True if find_spec("numpy") else False + file_dir = os.path.split(__file__)[0] samples_dir = os.path.join(file_dir, "../samples") frames_dir = os.path.join(samples_dir, "frames") @@ -265,9 +255,9 @@ def test_skip_first(): apngasm = APNGAsm() apngasm.add_frame_from_file(elephant_frame0_path) apngasm.set_skip_first(True) - assert apngasm.is_skip_first() == True + assert apngasm.is_skip_first() is True apngasm.set_skip_first(False) - assert apngasm.is_skip_first() == False + assert apngasm.is_skip_first() is False def test_get_frames(): From 885495fcb66ecc42f2213fd0bb3dc937fddce1dd Mon Sep 17 00:00:00 2001 From: laggykiller Date: Wed, 28 Feb 2024 18:39:14 +0800 Subject: [PATCH 067/103] Fix build --- scripts/get_deps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/get_deps.py b/scripts/get_deps.py index 8864214..2e3aac6 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -116,7 +116,7 @@ def patch_conan_toolchain_universal2(lipo_dir_merge_src: str): else: result += line - with open(conan_toolchain_path, "w_") as f: + with open(conan_toolchain_path, "w+") as f: f.write(result) From 485e9e5ca11eb8d1bc944b74c9f4d45f5732c170 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Wed, 28 Feb 2024 18:58:05 +0800 Subject: [PATCH 068/103] Remove conan_output rmtree as not necessary --- scripts/get_deps.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/get_deps.py b/scripts/get_deps.py index 2e3aac6..06c1cf2 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -86,7 +86,6 @@ def install_deps(arch: str): subprocess.run(["conan", "profile", "detect", "-f"]) conan_output = os.path.join("conan_output", arch) - shutil.rmtree(conan_output, ignore_errors=True) subprocess.run( [ From 075bccb9b4e7684cccf3058720f38c68125b0d38 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Thu, 29 Feb 2024 01:17:42 +0800 Subject: [PATCH 069/103] Build with macos deployment target 10.9 --- .github/workflows/build.yml | 15 +++------------ CMakeLists.txt | 3 ++- apngasm | 2 +- scripts/get_deps.py | 2 +- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d93a74a..807ba5d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,24 +56,15 @@ jobs: - os: macos-12 cibw_archs: x86_64 cibw_build: "*" - cibw_environment: > - APNGASM_COMPILE_TARGET=x86_64 - _PYTHON_HOST_PLATFORM=macosx-10.15-x86_64 - MACOSX_DEPLOYMENT_TARGET=10.15 + cibw_environment: APNGASM_COMPILE_TARGET=x86_64 - os: macos-14 cibw_archs: arm64 cibw_build: "*" - cibw_environment: > - APNGASM_COMPILE_TARGET=armv8 - _PYTHON_HOST_PLATFORM=macosx-11.0-arm64 - MACOSX_DEPLOYMENT_TARGET=11.0 + cibw_environment: APNGASM_COMPILE_TARGET=armv8 - os: macos-14 cibw_archs: universal2 cibw_build: "*" - cibw_environment: > - APNGASM_COMPILE_TARGET=universal2 - _PYTHON_HOST_PLATFORM=macosx-10.15-universal2 - MACOSX_DEPLOYMENT_TARGET=10.15 + cibw_environment: APNGASM_COMPILE_TARGET=universal2 - os: ubuntu-20.04 cibw_archs: x86_64 cibw_build: "*" diff --git a/CMakeLists.txt b/CMakeLists.txt index f56eb85..8e10094 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,8 @@ elseif (APPLE) if(${APNGASM_COMPILE_TARGET} STREQUAL "armv8") set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0") else() - set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15") + set(CMAKE_OSX_DEPLOYMENT_TARGET "10.9") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x -stdlib=libc++") endif() endif() diff --git a/apngasm b/apngasm index c983a42..19d4ee1 160000 --- a/apngasm +++ b/apngasm @@ -1 +1 @@ -Subproject commit c983a42c17b1625b6c6d7fb6e36c756757e499a8 +Subproject commit 19d4ee1366bf94fe41856d7204deff4f6db401aa diff --git a/scripts/get_deps.py b/scripts/get_deps.py index 06c1cf2..823586f 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -21,7 +21,7 @@ def install_deps(arch: str): if arch == "armv8": settings.append("os.version=11.0") else: - settings.append("os.version=10.15") + settings.append("os.version=10.9") settings.append("compiler=apple-clang") settings.append("compiler.libcxx=libc++") elif platform.system() == "Linux": From f15f5cc1f708ae7b27720d3a02e8507d7c191519 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Thu, 29 Feb 2024 01:22:38 +0800 Subject: [PATCH 070/103] Also install boost filesystem --- scripts/get_deps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/get_deps.py b/scripts/get_deps.py index 823586f..471a993 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -42,7 +42,7 @@ def install_deps(arch: str): options.append("boost/*:without_date_time=True") options.append("boost/*:without_exception=True") options.append("boost/*:without_fiber=True") - options.append("boost/*:without_filesystem=True") + options.append("boost/*:without_filesystem=False") options.append("boost/*:without_graph=True") options.append("boost/*:without_graph_parallel=True") options.append("boost/*:without_iostreams=True") From 249f43017fefd710fcd0d3419bd0b34dc25ace4d Mon Sep 17 00:00:00 2001 From: laggykiller Date: Thu, 29 Feb 2024 01:25:05 +0800 Subject: [PATCH 071/103] Also install boost atomic as dep of filesystem --- scripts/get_deps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/get_deps.py b/scripts/get_deps.py index 471a993..8ddd7b6 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -32,7 +32,7 @@ def install_deps(arch: str): if arch: settings.append("arch=" + arch) - options.append("boost/*:without_atomic=True") + options.append("boost/*:without_atomic=False") options.append("boost/*:without_chrono=True") options.append("boost/*:without_cobalt=True") options.append("boost/*:without_container=True") From 5f09ff1e14d5b24676baaf049e72a579fe9db185 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Thu, 29 Feb 2024 01:47:20 +0800 Subject: [PATCH 072/103] Update apngasm submodule --- apngasm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apngasm b/apngasm index 19d4ee1..060bfc1 160000 --- a/apngasm +++ b/apngasm @@ -1 +1 @@ -Subproject commit 19d4ee1366bf94fe41856d7204deff4f6db401aa +Subproject commit 060bfc19c135a53eb1d3c4294cc89402ccd698aa From 9205c1d37209ef9b3f6237788189c6223a394169 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Thu, 29 Feb 2024 02:27:57 +0800 Subject: [PATCH 073/103] Fix test --- tests/test_binder.py | 17 ++++++++--------- tests/test_direct.py | 17 ++++++++--------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/tests/test_binder.py b/tests/test_binder.py index be7988e..64ea124 100644 --- a/tests/test_binder.py +++ b/tests/test_binder.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 from importlib.util import find_spec import os -import sys import pytest @@ -304,31 +303,31 @@ def test_load_animation_spec_json(): assert len(frames) == 2 -@pytest.mark.skipif( - sys.platform == "win32", reason="Bug in apngasm causing incorrect path" -) +# @pytest.mark.skipif( +# sys.platform == "win32", reason="Bug in apngasm causing incorrect path" +# ) def test_save_json(tmpdir): apngasm = APNGAsmBinder() apngasm.add_frame_from_file(animation_spec_0_png_path) apngasm.add_frame_from_file(animation_spec_1_png_path) out = os.path.join(tmpdir, "animation_spec.json") - apngasm.save_json(out, input_dir) + apngasm.save_json(out, str(tmpdir)) with open(out) as f, open(animation_spec_json) as g: assert f.read() == g.read() -@pytest.mark.skipif( - sys.platform == "win32", reason="Bug in apngasm causing incorrect path" -) +# @pytest.mark.skipif( +# sys.platform == "win32", reason="Bug in apngasm causing incorrect path" +# ) def test_save_xml(tmpdir): apngasm = APNGAsmBinder() apngasm.add_frame_from_file(animation_spec_0_png_path) apngasm.add_frame_from_file(animation_spec_1_png_path) out = os.path.join(tmpdir, "animation_spec.xml") - apngasm.save_xml(out, input_dir) + apngasm.save_xml(out, str(tmpdir)) with open(out) as f, open(animation_spec_xml) as g: assert f.read() == g.read() diff --git a/tests/test_direct.py b/tests/test_direct.py index fc6ca9b..90ed3d9 100644 --- a/tests/test_direct.py +++ b/tests/test_direct.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 from importlib.util import find_spec import os -import sys import pytest @@ -214,31 +213,31 @@ def test_load_animation_spec_json(): assert len(frames) == 2 -@pytest.mark.skipif( - sys.platform == "win32", reason="Bug in apngasm causing incorrect path" -) +# @pytest.mark.skipif( +# sys.platform == "win32", reason="Bug in apngasm causing incorrect path" +# ) def test_save_json(tmpdir): apngasm = APNGAsm() apngasm.add_frame_from_file(animation_spec_0_png_path) apngasm.add_frame_from_file(animation_spec_1_png_path) out = os.path.join(tmpdir, "animation_spec.json") - apngasm.save_json(out, input_dir) + apngasm.save_json(out, str(tmpdir)) with open(out) as f, open(animation_spec_json) as g: assert f.read() == g.read() -@pytest.mark.skipif( - sys.platform == "win32", reason="Bug in apngasm causing incorrect path" -) +# @pytest.mark.skipif( +# sys.platform == "win32", reason="Bug in apngasm causing incorrect path" +# ) def test_save_xml(tmpdir): apngasm = APNGAsm() apngasm.add_frame_from_file(animation_spec_0_png_path) apngasm.add_frame_from_file(animation_spec_1_png_path) out = os.path.join(tmpdir, "animation_spec.xml") - apngasm.save_xml(out, input_dir) + apngasm.save_xml(out, str(tmpdir)) with open(out) as f, open(animation_spec_xml) as g: assert f.read() == g.read() From 0de1e1969bfc79593f9c9a4fcdcefd97fe11cd3e Mon Sep 17 00:00:00 2001 From: laggykiller Date: Thu, 29 Feb 2024 02:28:23 +0800 Subject: [PATCH 074/103] Do not set extra flags and deployment target for macos --- CMakeLists.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e10094..ef72b17 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,13 +28,13 @@ if (WIN32) set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd") elseif (LINUX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") -elseif (APPLE) - if(${APNGASM_COMPILE_TARGET} STREQUAL "armv8") - set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0") - else() - set(CMAKE_OSX_DEPLOYMENT_TARGET "10.9") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x -stdlib=libc++") - endif() +# elseif (APPLE) +# if(${APNGASM_COMPILE_TARGET} STREQUAL "armv8") +# set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0") +# else() +# set(CMAKE_OSX_DEPLOYMENT_TARGET "10.9") +# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x -stdlib=libc++") +# endif() endif() set(ZLIB_USE_STATIC_LIBS ON) From e6b0ff50c07c9a1b29b12b87fe59b493914537a4 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Thu, 29 Feb 2024 02:58:51 +0800 Subject: [PATCH 075/103] Compile with `-fno-aligned-allocation` --- CMakeLists.txt | 14 +++++++------- apngasm | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ef72b17..e1e9f51 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,13 +28,13 @@ if (WIN32) set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd") elseif (LINUX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") -# elseif (APPLE) -# if(${APNGASM_COMPILE_TARGET} STREQUAL "armv8") -# set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0") -# else() -# set(CMAKE_OSX_DEPLOYMENT_TARGET "10.9") -# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x -stdlib=libc++") -# endif() +elseif (APPLE) + if(${APNGASM_COMPILE_TARGET} STREQUAL "armv8") + set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0") + else() + set(CMAKE_OSX_DEPLOYMENT_TARGET "10.9") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x -stdlib=libc++ -fno-aligned-allocation") + endif() endif() set(ZLIB_USE_STATIC_LIBS ON) diff --git a/apngasm b/apngasm index 060bfc1..c983a42 160000 --- a/apngasm +++ b/apngasm @@ -1 +1 @@ -Subproject commit 060bfc19c135a53eb1d3c4294cc89402ccd698aa +Subproject commit c983a42c17b1625b6c6d7fb6e36c756757e499a8 From dc274593bf7cf03c6f9d755d0ce10752316f247e Mon Sep 17 00:00:00 2001 From: laggykiller Date: Thu, 29 Feb 2024 03:07:22 +0800 Subject: [PATCH 076/103] switch back to osx10.9-compat branch of apngasm --- apngasm | 2 +- scripts/get_deps.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apngasm b/apngasm index c983a42..060bfc1 160000 --- a/apngasm +++ b/apngasm @@ -1 +1 @@ -Subproject commit c983a42c17b1625b6c6d7fb6e36c756757e499a8 +Subproject commit 060bfc19c135a53eb1d3c4294cc89402ccd698aa diff --git a/scripts/get_deps.py b/scripts/get_deps.py index 8ddd7b6..3ae35ef 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -32,7 +32,7 @@ def install_deps(arch: str): if arch: settings.append("arch=" + arch) - options.append("boost/*:without_atomic=False") + options.append("boost/*:without_atomic=False") # Depedency for filesystem options.append("boost/*:without_chrono=True") options.append("boost/*:without_cobalt=True") options.append("boost/*:without_container=True") @@ -42,7 +42,7 @@ def install_deps(arch: str): options.append("boost/*:without_date_time=True") options.append("boost/*:without_exception=True") options.append("boost/*:without_fiber=True") - options.append("boost/*:without_filesystem=False") + options.append("boost/*:without_filesystem=False") # Required by osx 10.9 fork options.append("boost/*:without_graph=True") options.append("boost/*:without_graph_parallel=True") options.append("boost/*:without_iostreams=True") From 236034e1a4a801ebe6ae2d4121afab1b7a92ca9d Mon Sep 17 00:00:00 2001 From: laggykiller <61652821+laggykiller@users.noreply.github.com> Date: Fri, 1 Mar 2024 17:04:35 +0800 Subject: [PATCH 077/103] Fix tmpdir typing in tests --- tests/test_binder.py | 15 +++++---------- tests/test_direct.py | 15 +++++---------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/tests/test_binder.py b/tests/test_binder.py index 64ea124..6b44a2e 100644 --- a/tests/test_binder.py +++ b/tests/test_binder.py @@ -3,6 +3,7 @@ import os import pytest +from _pytest._py.path import LocalPath from apngasm_python.apngasm import APNGAsmBinder from apngasm_python._apngasm_python import APNGFrame @@ -246,7 +247,7 @@ def test_add_frame_from_numpy_non_rgb(): apngasm.add_frame_from_numpy(arr) -def test_assemble(tmpdir): +def test_assemble(tmpdir: LocalPath): apngasm = APNGAsmBinder() apngasm.add_frame_from_file(grey_png_path) apngasm.add_frame_from_file(palette_png_path) @@ -281,7 +282,7 @@ def test_disassemble_as_pillow(): assert isinstance(im, Image.Image) -def test_save_pngs(tmpdir): +def test_save_pngs(tmpdir: LocalPath): apngasm = APNGAsmBinder() apngasm.add_frame_from_file(elephant_frame0_path) apngasm.save_pngs(str(tmpdir)) @@ -303,10 +304,7 @@ def test_load_animation_spec_json(): assert len(frames) == 2 -# @pytest.mark.skipif( -# sys.platform == "win32", reason="Bug in apngasm causing incorrect path" -# ) -def test_save_json(tmpdir): +def test_save_json(tmpdir: LocalPath): apngasm = APNGAsmBinder() apngasm.add_frame_from_file(animation_spec_0_png_path) apngasm.add_frame_from_file(animation_spec_1_png_path) @@ -318,10 +316,7 @@ def test_save_json(tmpdir): assert f.read() == g.read() -# @pytest.mark.skipif( -# sys.platform == "win32", reason="Bug in apngasm causing incorrect path" -# ) -def test_save_xml(tmpdir): +def test_save_xml(tmpdir: LocalPath): apngasm = APNGAsmBinder() apngasm.add_frame_from_file(animation_spec_0_png_path) apngasm.add_frame_from_file(animation_spec_1_png_path) diff --git a/tests/test_direct.py b/tests/test_direct.py index 90ed3d9..750a4f0 100644 --- a/tests/test_direct.py +++ b/tests/test_direct.py @@ -3,6 +3,7 @@ import os import pytest +from _pytest._py.path import LocalPath from apngasm_python._apngasm_python import ( APNGAsm, @@ -171,7 +172,7 @@ def test_add_frame_from_file_palette(): assert frame_count == 1 -def test_assemble(tmpdir): +def test_assemble(tmpdir: LocalPath): apngasm = APNGAsm() apngasm.add_frame_from_file(grey_png_path) apngasm.add_frame_from_file(palette_png_path) @@ -191,7 +192,7 @@ def test_disassemble(): assert isinstance(frame, APNGFrame) -def test_save_pngs(tmpdir): +def test_save_pngs(tmpdir: LocalPath): apngasm = APNGAsm() apngasm.add_frame_from_file(elephant_frame0_path) apngasm.save_pngs(str(tmpdir)) @@ -213,10 +214,7 @@ def test_load_animation_spec_json(): assert len(frames) == 2 -# @pytest.mark.skipif( -# sys.platform == "win32", reason="Bug in apngasm causing incorrect path" -# ) -def test_save_json(tmpdir): +def test_save_json(tmpdir: LocalPath): apngasm = APNGAsm() apngasm.add_frame_from_file(animation_spec_0_png_path) apngasm.add_frame_from_file(animation_spec_1_png_path) @@ -228,10 +226,7 @@ def test_save_json(tmpdir): assert f.read() == g.read() -# @pytest.mark.skipif( -# sys.platform == "win32", reason="Bug in apngasm causing incorrect path" -# ) -def test_save_xml(tmpdir): +def test_save_xml(tmpdir: LocalPath): apngasm = APNGAsm() apngasm.add_frame_from_file(animation_spec_0_png_path) apngasm.add_frame_from_file(animation_spec_1_png_path) From 34e288df984438e851fb271f8714ed9679a639be Mon Sep 17 00:00:00 2001 From: laggykiller <61652821+laggykiller@users.noreply.github.com> Date: Fri, 1 Mar 2024 19:27:11 +0800 Subject: [PATCH 078/103] Fix compiling on cygwin --- CMakeLists.txt | 9 +++++---- PreLoad.cmake | 7 +++++++ pyproject.toml | 3 +-- scripts/get_deps.py | 10 +++++++++- src/apngasm_python.cpp | 12 ++++++++---- 5 files changed, 30 insertions(+), 11 deletions(-) create mode 100644 PreLoad.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index e1e9f51..875dd42 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,10 +22,11 @@ execute_process( ) message(STATUS "Finished get_deps.py") -if (WIN32) +if (MSVC) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd") + set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} /Zi /Ob0 /Od /RTC1") elseif (LINUX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") elseif (APPLE) @@ -40,7 +41,7 @@ endif() set(ZLIB_USE_STATIC_LIBS ON) set(PNG_USE_STATIC_LIBS ON) set(Boost_USE_STATIC_LIBS ON) -if (WIN32) +if (MSVC) set(Boost_USE_MULTITHREADED ON) set(Boost_USE_STATIC_RUNTIME ON) endif() @@ -78,7 +79,7 @@ find_nanobind_python_first() # Compile the Python module nanobind_add_module(_apngasm_python "src/apngasm_python.cpp" NB_STATIC STABLE_ABI) -if (WIN32) +if (MSVC) nanobind_compile_options(_apngasm_python "/MT /MP /bigobj") endif() target_compile_definitions(_apngasm_python PRIVATE _apngasm_python_EXPORTS) @@ -103,7 +104,7 @@ set_target_properties(_apngasm_python PROPERTIES CXX_VISIBILITY_PRESET "hidden" VISIBILITY_INLINES_HIDDEN true POSITION_INDEPENDENT_CODE true) -if (WIN32) +if (MSVC) set_target_properties(_apngasm_python PROPERTIES LINK_FALGS_RELEASE "/WHOLEARCHIVE:MNN") elseif (LINUX) target_link_options(_apngasm_python PRIVATE "LINKER:--exclude-libs,ALL") diff --git a/PreLoad.cmake b/PreLoad.cmake new file mode 100644 index 0000000..ce5c9a4 --- /dev/null +++ b/PreLoad.cmake @@ -0,0 +1,7 @@ +# https://stackoverflow.com/a/45247784 +# Need python headers and libraries, but msvc not able to find them +# If inside cygwin or msys. + +if (WIN32 AND NOT MSVC) + set (CMAKE_GENERATOR "Unix Makefiles" CACHE INTERNAL "" FORCE) +endif() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3a48dd1..d3ee6ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ directory = "src-python" [tool.py-build-cmake.sdist] # What to include in source distributions include = [ "CMakeLists.txt", + "PreLoad.cmake", "cmake/*.cmake", "src/*", "src-python/*", @@ -63,8 +64,6 @@ config = "Release" [tool.py-build-cmake.windows.cmake] # Windows-specific options config = "Release" -[tool.py-build-cmake.windows.cmake.options] -CMAKE_CXX_FLAGS_RELWITHDEBINFO = "/Zi /Ob0 /Od /RTC1" [tool.cibuildwheel] build-verbosity = 1 diff --git a/scripts/get_deps.py b/scripts/get_deps.py index 3ae35ef..ea7917c 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -3,6 +3,7 @@ import os import subprocess import shutil +import sys from get_arch import conan_archs, get_arch @@ -15,7 +16,14 @@ def install_deps(arch: str): if platform.system() == "Windows": settings.append("os=Windows") - settings.append("compiler.runtime=static") + if sys.platform.startswith(("cygwin", "msys")) or shutil.which("cygcheck"): + # Need python headers and libraries, but msvc not able to find them + # If inside cygwin or msys. + settings.append("compiler=gcc") + settings.append("compiler.version=10") + settings.append("compiler.libcxx=libstdc++") + else: + settings.append("compiler.runtime=static") elif platform.system() == "Darwin": settings.append("os=Macos") if arch == "armv8": diff --git a/src/apngasm_python.cpp b/src/apngasm_python.cpp index c0492a8..22aba0f 100644 --- a/src/apngasm_python.cpp +++ b/src/apngasm_python.cpp @@ -1,8 +1,12 @@ -#ifdef _WINDOWS -# ifdef _apngasm_python_EXPORTS -# define APNGASM_PY_DECLSPEC __declspec(dllexport) +#if defined _WIN32 || defined __CYGWIN__ +# ifdef __GNUC__ +# define APNGASM_PY_DECLSPEC __attribute__ ((visibility("default"))) # else -# define APNGASM_PY_DECLSPEC __declspec(dllimport) +# ifdef _apngasm_python_EXPORTS +# define APNGASM_PY_DECLSPEC __declspec(dllexport) +# else +# define APNGASM_PY_DECLSPEC __declspec(dllimport) +# endif # endif #else # define APNGASM_PY_DECLSPEC __attribute__ ((visibility("default"))) From fa1c57aa3274e310a759c15052283d56f14fa130 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 1 Mar 2024 11:30:34 +0000 Subject: [PATCH 079/103] Update stub and formatting --- scripts/get_deps.py | 4 ++-- src-python/apngasm_python/__init__.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/get_deps.py b/scripts/get_deps.py index ea7917c..24b90e0 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -40,7 +40,7 @@ def install_deps(arch: str): if arch: settings.append("arch=" + arch) - options.append("boost/*:without_atomic=False") # Depedency for filesystem + options.append("boost/*:without_atomic=False") # Depedency for filesystem options.append("boost/*:without_chrono=True") options.append("boost/*:without_cobalt=True") options.append("boost/*:without_container=True") @@ -50,7 +50,7 @@ def install_deps(arch: str): options.append("boost/*:without_date_time=True") options.append("boost/*:without_exception=True") options.append("boost/*:without_fiber=True") - options.append("boost/*:without_filesystem=False") # Required by osx 10.9 fork + options.append("boost/*:without_filesystem=False") # Required by osx 10.9 fork options.append("boost/*:without_graph=True") options.append("boost/*:without_graph_parallel=True") options.append("boost/*:without_iostreams=True") diff --git a/src-python/apngasm_python/__init__.py b/src-python/apngasm_python/__init__.py index 225e5e5..5b8043f 100755 --- a/src-python/apngasm_python/__init__.py +++ b/src-python/apngasm_python/__init__.py @@ -1,3 +1,4 @@ #!/usr/bin/env python3 """apngasm-python""" + __version__ = "1.3.0" From 451feb5ce7cbf07a450c0408581c354def7bec15 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Fri, 1 Mar 2024 20:58:12 +0800 Subject: [PATCH 080/103] Simplify declspec logic --- src/apngasm_python.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/apngasm_python.cpp b/src/apngasm_python.cpp index 22aba0f..9fac0d5 100644 --- a/src/apngasm_python.cpp +++ b/src/apngasm_python.cpp @@ -1,12 +1,8 @@ -#if defined _WIN32 || defined __CYGWIN__ -# ifdef __GNUC__ -# define APNGASM_PY_DECLSPEC __attribute__ ((visibility("default"))) +#if defined(_WIN32) && !defined(__GNUC__) +# ifdef _apngasm_python_EXPORTS +# define APNGASM_PY_DECLSPEC __declspec(dllexport) # else -# ifdef _apngasm_python_EXPORTS -# define APNGASM_PY_DECLSPEC __declspec(dllexport) -# else -# define APNGASM_PY_DECLSPEC __declspec(dllimport) -# endif +# define APNGASM_PY_DECLSPEC __declspec(dllimport) # endif #else # define APNGASM_PY_DECLSPEC __attribute__ ((visibility("default"))) From e0a1710e574af1dcfe78e24071b69a94fdbe8362 Mon Sep 17 00:00:00 2001 From: laggykiller <61652821+laggykiller@users.noreply.github.com> Date: Fri, 1 Mar 2024 22:14:12 +0800 Subject: [PATCH 081/103] Improve ease of building on cygwin, mingw and msys --- PreLoad.cmake | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/PreLoad.cmake b/PreLoad.cmake index ce5c9a4..88876c0 100644 --- a/PreLoad.cmake +++ b/PreLoad.cmake @@ -3,5 +3,12 @@ # If inside cygwin or msys. if (WIN32 AND NOT MSVC) - set (CMAKE_GENERATOR "Unix Makefiles" CACHE INTERNAL "" FORCE) + execute_process(COMMAND uname OUTPUT_VARIABLE uname) + if (uname MATCHES "^MINGW") + set (CMAKE_GENERATOR "MinGW Makefiles" CACHE INTERNAL "" FORCE) + elseif (uname MATCHES "^MSYS") + set (CMAKE_GENERATOR "MSYS Makefiles" CACHE INTERNAL "" FORCE) + else () + set (CMAKE_GENERATOR "Ninja" CACHE INTERNAL "" FORCE) + endif() endif() \ No newline at end of file From 299c1a98bb625e09a5a04ea826326cf1ea4a0aeb Mon Sep 17 00:00:00 2001 From: laggykiller Date: Fri, 1 Mar 2024 22:25:53 +0800 Subject: [PATCH 082/103] Only build abi3 wheels for minimum supported cpython version --- .github/workflows/build.yml | 24 ++++++++++++------------ pyproject.toml | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 807ba5d..f83dba8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,51 +43,51 @@ jobs: include: - os: windows-2019 cibw_archs: AMD64 - cibw_build: "*" + cibw_build: "cp38* pp*" cibw_environment: APNGASM_COMPILE_TARGET=x86_64 - os: windows-2019 cibw_archs: x86 - cibw_build: "*" + cibw_build: "cp38*" cibw_environment: APNGASM_COMPILE_TARGET=x86 - os: windows-2019 cibw_archs: ARM64 - cibw_build: "*" + cibw_build: "cp39*" cibw_environment: APNGASM_COMPILE_TARGET=armv8 - os: macos-12 cibw_archs: x86_64 - cibw_build: "*" + cibw_build: "cp38* pp*" cibw_environment: APNGASM_COMPILE_TARGET=x86_64 - os: macos-14 cibw_archs: arm64 - cibw_build: "*" + cibw_build: "cp38* pp*" cibw_environment: APNGASM_COMPILE_TARGET=armv8 - os: macos-14 cibw_archs: universal2 - cibw_build: "*" + cibw_build: "cp38* pp*" cibw_environment: APNGASM_COMPILE_TARGET=universal2 - os: ubuntu-20.04 cibw_archs: x86_64 - cibw_build: "*" + cibw_build: "cp38* pp*" cibw_environment: APNGASM_COMPILE_TARGET=x86_64 - os: ubuntu-20.04 cibw_archs: i686 - cibw_build: "*" + cibw_build: "cp38* pp*" cibw_environment: APNGASM_COMPILE_TARGET=x86 - os: ubuntu-20.04 cibw_archs: aarch64 - cibw_build: "*-musllinux_*" + cibw_build: "cp38*-musllinux_* pp*-musllinux_*" cibw_environment: APNGASM_COMPILE_TARGET=armv8 - os: ubuntu-20.04 cibw_archs: aarch64 - cibw_build: "*-manylinux_*" + cibw_build: "cp38*-manylinux_* pp*-manylinux_*" cibw_environment: APNGASM_COMPILE_TARGET=armv8 - os: ubuntu-20.04 cibw_archs: ppc64le - cibw_build: "*-musllinux_*" + cibw_build: "cp38*-musllinux_*" cibw_environment: APNGASM_COMPILE_TARGET=ppc64le - os: ubuntu-20.04 cibw_archs: ppc64le - cibw_build: "*-manylinux_*" + cibw_build: "cp38*-manylinux_*" cibw_environment: APNGASM_COMPILE_TARGET=ppc64le # - os: ubuntu-20.04 # cibw_archs: s390x diff --git a/pyproject.toml b/pyproject.toml index d3ee6ee..9df557f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ install_args = ["--verbose"] install_components = ["python_modules"] env = {} python_abi = 'abi3' -abi3_minimum_cpython_version = 312 +abi3_minimum_cpython_version = 38 [tool.py-build-cmake.linux.cmake] # Linux-specific options config = "Release" From 8dffcded61ee986b1ab660cc414e61f804390b72 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Fri, 1 Mar 2024 23:45:18 +0800 Subject: [PATCH 083/103] Set abi3_minimum_cpython_version back to 312; Run abi3audit --- .github/workflows/build.yml | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f83dba8..520a245 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -117,10 +117,10 @@ jobs: # CIBW_ENVIRONMENT: PY_BUILD_CMAKE_VERBOSE=1 ${{ matrix.cibw_environment }} CIBW_ENVIRONMENT: ${{ matrix.cibw_environment }} CIBW_BUILD: ${{ matrix.cibw_build }} - CIBW_TEST_REQUIRES: pytest + CIBW_TEST_REQUIRES: pytest abi3audit CIBW_BEFORE_TEST: pip install --only-binary ":all:" Pillow numpy; true CIBW_BEFORE_TEST_WINDOWS: pip install --only-binary ":all:" Pillow numpy || VER>NUL - CIBW_TEST_COMMAND: pytest {package}/tests + CIBW_TEST_COMMAND: pytest {package}/tests && abi3audit {package}/wheelhouse/*.whl CIBW_TEST_SKIP: pp* - uses: actions/upload-artifact@v3 diff --git a/pyproject.toml b/pyproject.toml index 9df557f..d3ee6ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ install_args = ["--verbose"] install_components = ["python_modules"] env = {} python_abi = 'abi3' -abi3_minimum_cpython_version = 38 +abi3_minimum_cpython_version = 312 [tool.py-build-cmake.linux.cmake] # Linux-specific options config = "Release" From f2f5e07addea1c95ed15719240126a3eb3e0041f Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sat, 2 Mar 2024 00:32:40 +0800 Subject: [PATCH 084/103] Fix abi3audit command in build.yml --- .github/workflows/build.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 520a245..a7c06c2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -117,11 +117,16 @@ jobs: # CIBW_ENVIRONMENT: PY_BUILD_CMAKE_VERBOSE=1 ${{ matrix.cibw_environment }} CIBW_ENVIRONMENT: ${{ matrix.cibw_environment }} CIBW_BUILD: ${{ matrix.cibw_build }} - CIBW_TEST_REQUIRES: pytest abi3audit + CIBW_TEST_REQUIRES: pytest CIBW_BEFORE_TEST: pip install --only-binary ":all:" Pillow numpy; true CIBW_BEFORE_TEST_WINDOWS: pip install --only-binary ":all:" Pillow numpy || VER>NUL - CIBW_TEST_COMMAND: pytest {package}/tests && abi3audit {package}/wheelhouse/*.whl + CIBW_TEST_COMMAND: pytest {package}/tests CIBW_TEST_SKIP: pp* + + - name: abi3audit + run: | + pip install abi3audit && + abi3audit ./wheelhouse/*.whl - uses: actions/upload-artifact@v3 with: From 276c8b4807af639e6dbefeefd22607ba517721cc Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sat, 2 Mar 2024 01:58:40 +0800 Subject: [PATCH 085/103] Use python 3.11 in github action --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a7c06c2..cec4d71 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -99,6 +99,10 @@ jobs: # cibw_environment: APNGASM_COMPILE_TARGET=s390x steps: + - uses: actions/setup-python@v5 + with: + python-version: 3.11 + - uses: actions/checkout@v3 with: submodules: true From 38c1a482b59b5e53b38ede68c7b3e0a0b0f811de Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sat, 2 Mar 2024 04:23:48 +0800 Subject: [PATCH 086/103] More robust abi3audit github action command --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cec4d71..0cf0f23 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -130,7 +130,7 @@ jobs: - name: abi3audit run: | pip install abi3audit && - abi3audit ./wheelhouse/*.whl + abi3audit $(ls ./dist/*.whl) --debug --verbose - uses: actions/upload-artifact@v3 with: From 30da52d45a47a507831fb61df84fd95b0d9a4377 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sat, 2 Mar 2024 04:26:36 +0800 Subject: [PATCH 087/103] Fix abi3audit github action step --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0cf0f23..a5eb3b3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -130,7 +130,7 @@ jobs: - name: abi3audit run: | pip install abi3audit && - abi3audit $(ls ./dist/*.whl) --debug --verbose + abi3audit $(ls ./wheelhouse/*.whl) --debug --verbose - uses: actions/upload-artifact@v3 with: From a3c29d43cc5d7e3020977d1b043153199d0e0fe1 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sat, 9 Mar 2024 17:34:20 +0800 Subject: [PATCH 088/103] Fix for latest nanobind --- src/apngasm_python.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/apngasm_python.cpp b/src/apngasm_python.cpp index 9fac0d5..30e2122 100644 --- a/src/apngasm_python.cpp +++ b/src/apngasm_python.cpp @@ -37,7 +37,7 @@ NB_MODULE(MODULE_NAME, m) { m.attr("__version__") = VERSION_INFO; m.def("create_frame_from_rgb", []( - nb::ndarray> *pixels, + nb::ndarray> *pixels, unsigned int width, unsigned int height, unsigned delayNum = apngasm::DEFAULT_FRAME_NUMERATOR, unsigned delayDen = apngasm::DEFAULT_FRAME_DENOMINATOR @@ -72,7 +72,7 @@ NB_MODULE(MODULE_NAME, m) { )pbdoc"); m.def("create_frame_from_rgb_trns", []( - nb::ndarray> *pixels, + nb::ndarray> *pixels, unsigned int width, unsigned int height, nb::ndarray> *trns_color, unsigned delayNum = apngasm::DEFAULT_FRAME_NUMERATOR, @@ -117,7 +117,7 @@ NB_MODULE(MODULE_NAME, m) { )pbdoc"); m.def("create_frame_from_rgba", []( - nb::ndarray> *pixels, + nb::ndarray> *pixels, unsigned int width, unsigned int height, unsigned delayNum = apngasm::DEFAULT_FRAME_NUMERATOR, unsigned delayDen = apngasm::DEFAULT_FRAME_DENOMINATOR @@ -257,9 +257,9 @@ NB_MODULE(MODULE_NAME, m) { [](apngasm::APNGFrame &t) APNGASM_PY_DECLSPEC { size_t rowbytes = rowbytesMap[t._colorType]; size_t shape[3] = { t._height, t._width, rowbytes }; - return nb::ndarray>(t._pixels, 3, shape); + return nb::ndarray>(t._pixels, 3, shape); }, - [](apngasm::APNGFrame &t, nb::ndarray> *v) APNGASM_PY_DECLSPEC { + [](apngasm::APNGFrame &t, nb::ndarray> *v) APNGASM_PY_DECLSPEC { size_t rowbytes = rowbytesMap[t._colorType]; unsigned char *pixelsNew = new unsigned char[v->size()]; unsigned char *v_ptr = v->data(); @@ -335,9 +335,9 @@ NB_MODULE(MODULE_NAME, m) { .def_prop_rw("transparency", [](apngasm::APNGFrame &t) APNGASM_PY_DECLSPEC { size_t shape[1] = { static_cast(t._transparencySize) }; - return nb::ndarray>(t._transparency, 1, shape); + return nb::ndarray>(t._transparency, 1, shape); }, - [](apngasm::APNGFrame &t, nb::ndarray> *v) APNGASM_PY_DECLSPEC { + [](apngasm::APNGFrame &t, nb::ndarray> *v) APNGASM_PY_DECLSPEC { unsigned char *v_ptr = v->data(); for (int i = 0; i < v->shape(0); ++i) { t._transparency[i] = *v_ptr; From d64ffa8307bd4ce9cc754e0113e053857a5adb8b Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sat, 9 Mar 2024 17:34:50 +0800 Subject: [PATCH 089/103] Move linting and formatting tools settings to pyproject.toml --- .github/workflows/check_and_fix.yml | 22 +++++-------- conanfile.py | 4 +-- example/example_binder.py | 11 ++++--- example/example_direct.py | 16 ++++------ pyproject.toml | 16 +++++++++- scripts/get_arch.py | 2 +- scripts/get_deps.py | 6 ++-- src-python/apngasm_python/_apngasm_python.pyi | 9 +++--- src-python/apngasm_python/apngasm.py | 28 ++++++++++------ tests/test_binder.py | 17 +++++----- tests/test_direct.py | 32 ++++++++----------- 11 files changed, 87 insertions(+), 76 deletions(-) diff --git a/.github/workflows/check_and_fix.yml b/.github/workflows/check_and_fix.yml index 85bd2be..fcdaeb1 100644 --- a/.github/workflows/check_and_fix.yml +++ b/.github/workflows/check_and_fix.yml @@ -21,27 +21,21 @@ jobs: id: extract_branch - uses: actions/setup-python@v5 with: - python-version: '3.8' + python-version: '3.9' - name: Install dependency run: | - pip install build ruff pyright Pillow numpy pytest + pip install build ruff mypy Pillow numpy pytest pip install git+https://github.com/wjakob/nanobind.git - name: Ruff check - run: | - ruff check src-python - ruff check scripts - ruff check tests - ruff check example + run: ruff check - name: Install test run: pip install . - - name: pyright - run: pyright + - name: mypy + run: mypy . - name: ruff format - run: | - ruff format src-python - ruff format scripts - ruff format tests - ruff format example + run: ruff format + - name: isort + run: isort . - name: nanobind stubgen run: | python -m nanobind.stubgen \ diff --git a/conanfile.py b/conanfile.py index bc65415..740b2aa 100644 --- a/conanfile.py +++ b/conanfile.py @@ -3,7 +3,7 @@ from conan import ConanFile import shutil from scripts.get_arch import get_arch -from conan.tools.cmake import CMake, CMakeToolchain, CMakeDeps, cmake_layout +from conan.tools.cmake import CMakeToolchain, CMakeDeps from conan.tools.apple import is_apple_os @@ -21,7 +21,7 @@ def build_requirements(self): self.tool_requires("cmake/[>=3.27]") def build(self): - build_type = "Release" + build_type = "Release" # noqa: F841 def generate(self): tc = CMakeToolchain(self) diff --git a/example/example_binder.py b/example/example_binder.py index 79bc6f3..a27243f 100755 --- a/example/example_binder.py +++ b/example/example_binder.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 -from apngasm_python.apngasm import APNGAsmBinder import os import shutil -from PIL import Image + import numpy as np +from apngasm_python.apngasm import APNGAsmBinder +from PIL import Image file_dir = os.path.split(__file__)[0] samples_dir = os.path.join(file_dir, "../samples") @@ -29,11 +30,11 @@ apngasm.add_frame_from_file(os.path.join(frames_dir, file_name), 100, 1000) # Getting information about one frame -frame = apngasm.get_frames()[0] +apng_frame = apngasm.get_frames()[0] # Saving one frame as file out = os.path.join(output_dir, "elephant-frame.png") -frame.save(out) +apng_frame.save(out) # Getting one frame as Pillow Image im = apngasm.frame_pixels_as_pillow(0) @@ -65,7 +66,7 @@ # Assemble from pillow images # Just for fun, let's also make it spin apngasm.reset() -angle = 0 +angle = 0.0 angle_step = 360 / len(os.listdir(frames_dir)) for file_name in sorted(os.listdir(frames_dir)): image = Image.open(os.path.join(frames_dir, file_name)) diff --git a/example/example_direct.py b/example/example_direct.py index 979a61a..6bef88a 100755 --- a/example/example_direct.py +++ b/example/example_direct.py @@ -1,15 +1,13 @@ #!/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, -) import os import shutil -from PIL import Image + import numpy as np +from apngasm_python._apngasm_python import (APNGAsm, APNGFrame, + create_frame_from_rgb, + create_frame_from_rgb_trns, + create_frame_from_rgba) +from PIL import Image file_dir = os.path.split(__file__)[0] samples_dir = os.path.join(file_dir, "../samples") @@ -96,7 +94,7 @@ def frame_info(frame: APNGFrame): # Assemble from pillow images # Just for fun, let's also make it spin apngasm.reset() -angle = 0 +angle = 0.0 angle_step = 360 / len(os.listdir(frames_dir)) for file_name in sorted(os.listdir(frames_dir)): image = Image.open(os.path.join(frames_dir, file_name)) diff --git a/pyproject.toml b/pyproject.toml index d3ee6ee..5ef6409 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,4 +67,18 @@ config = "Release" [tool.cibuildwheel] build-verbosity = 1 -environment = { PY_BUILD_CMAKE_VERBOSE="1" } \ No newline at end of file +environment = { PY_BUILD_CMAKE_VERBOSE="1" } + +[tool.pyright] +include = ["src-python", "scripts", "tests", "example"] +strict = ["*"] + +[tool.mypy] +python_version = "3.9" +exclude = ["lipo-dir-merge"] + +[tool.isort] +extend_skip = ["lipo-dir-merge"] + +[tool.ruff] +exclude = ["lipo-dir-merge"] \ No newline at end of file diff --git a/scripts/get_arch.py b/scripts/get_arch.py index 116f455..2438678 100755 --- a/scripts/get_arch.py +++ b/scripts/get_arch.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import os -import sys import platform +import sys conan_archs = { "x86_64": ["amd64", "x86_64", "x64"], diff --git a/scripts/get_deps.py b/scripts/get_deps.py index 24b90e0..12ae2cf 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 -import platform import os -import subprocess +import platform import shutil +import subprocess import sys -from get_arch import conan_archs, get_arch +from get_arch import conan_archs, get_arch # type: ignore def install_deps(arch: str): diff --git a/src-python/apngasm_python/_apngasm_python.pyi b/src-python/apngasm_python/_apngasm_python.pyi index 4351721..e3d99bb 100644 --- a/src-python/apngasm_python/_apngasm_python.pyi +++ b/src-python/apngasm_python/_apngasm_python.pyi @@ -1,5 +1,6 @@ +from collections.abc import Sequence from numpy.typing import ArrayLike -from typing import overload, Sequence, Optional, List, Annotated +from typing import overload, Optional, Annotated class APNGAsm: """Class representing APNG file, storing APNGFrame(s) and other metadata.""" @@ -88,7 +89,7 @@ class APNGAsm: :rtype: bool """ - def disassemble(self, file_path: str) -> List[APNGFrame]: + def disassemble(self, file_path: str) -> list[APNGFrame]: """ Disassembles an APNG file. @@ -108,7 +109,7 @@ class APNGAsm: :rtype: bool """ - def load_animation_spec(self, file_path: str) -> List[APNGFrame]: + 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. @@ -164,7 +165,7 @@ class APNGAsm: :param int skip_first: Flag of skip first frame. """ - def get_frames(self) -> List[APNGFrame]: + def get_frames(self) -> list[APNGFrame]: """ Returns the frame vector. diff --git a/src-python/apngasm_python/apngasm.py b/src-python/apngasm_python/apngasm.py index 218b385..df6131e 100755 --- a/src-python/apngasm_python/apngasm.py +++ b/src-python/apngasm_python/apngasm.py @@ -1,19 +1,16 @@ #!/usr/bin/env python3 from __future__ import annotations -from typing import Optional, Any, TYPE_CHECKING + +from typing import TYPE_CHECKING, Any, Optional if TYPE_CHECKING: from numpy.typing import NDArray from PIL import Image -from ._apngasm_python import ( # type: ignore - APNGAsm, - APNGFrame, - IAPNGAsmListener, - create_frame_from_rgb, - create_frame_from_rgb_trns, - create_frame_from_rgba, -) +from ._apngasm_python import (APNGAsm, APNGFrame, # type: ignore + IAPNGAsmListener, create_frame_from_rgb, + create_frame_from_rgb_trns, + create_frame_from_rgba) class APNGAsmBinder: @@ -52,6 +49,7 @@ def frame_pixels_as_pillow( if new_value: self.apngasm.get_frames()[frame].pixels = array(new_value) + return None else: mode = self.color_type_dict[self.apngasm.get_frames()[frame].color_type] return Image.frombytes( # type: ignore @@ -82,6 +80,7 @@ def frame_pixels_as_numpy( if new_value: self.apngasm.get_frames()[frame].pixels = new_value + return None else: return array(self.apngasm.get_frames()[frame].pixels) @@ -98,6 +97,7 @@ def frame_width(self, frame: int, new_value: Optional[int] = None) -> Optional[i """ if new_value: self.apngasm.get_frames()[frame].width = new_value + return None else: return self.apngasm.get_frames()[frame].width @@ -116,6 +116,7 @@ def frame_height( """ if new_value: self.apngasm.get_frames()[frame].height = new_value + return None else: return self.apngasm.get_frames()[frame].height @@ -140,6 +141,7 @@ def frame_color_type( """ if new_value: self.apngasm.get_frames()[frame].color_type = new_value + return None else: return self.apngasm.get_frames()[frame].color_type @@ -164,6 +166,7 @@ def frame_palette( if new_value: self.apngasm.get_frames()[frame].palette = new_value + return None else: return array(self.apngasm.get_frames()[frame].palette) @@ -188,6 +191,7 @@ def frame_transparency( if new_value: self.apngasm.get_frames()[frame].transparency = new_value + return None else: return array(self.apngasm.get_frames()[frame].transparency) @@ -206,6 +210,7 @@ def frame_palette_size( """ if new_value: self.apngasm.get_frames()[frame].palette_size = new_value + return None else: return self.apngasm.get_frames()[frame].palette_size @@ -224,6 +229,7 @@ def frame_transparency_size( """ if new_value: self.apngasm.get_frames()[frame].transparency_size = new_value + return None else: return self.apngasm.get_frames()[frame].transparency_size @@ -243,6 +249,7 @@ def frame_delay_num( """ if new_value: self.apngasm.get_frames()[frame].delay_num = new_value + return None else: return self.apngasm.get_frames()[frame].delay_num @@ -262,6 +269,7 @@ def frame_delay_den( """ if new_value: self.apngasm.get_frames()[frame].delay_den = new_value + return None else: return self.apngasm.get_frames()[frame].delay_den @@ -343,7 +351,7 @@ def add_frame_from_numpy( :return: The new number of frames. :rtype: int """ - from numpy import shape, ndarray + from numpy import ndarray, shape width = width if width else shape(numpy_data)[1] height = height if height else shape(numpy_data)[0] diff --git a/tests/test_binder.py b/tests/test_binder.py index 6b44a2e..b6316fc 100644 --- a/tests/test_binder.py +++ b/tests/test_binder.py @@ -1,12 +1,11 @@ #!/usr/bin/env python3 -from importlib.util import find_spec import os +from importlib.util import find_spec import pytest from _pytest._py.path import LocalPath - -from apngasm_python.apngasm import APNGAsmBinder from apngasm_python._apngasm_python import APNGFrame +from apngasm_python.apngasm import APNGAsmBinder PILLOW_LOADED = True if find_spec("PIL") else False NUMPY_LOADED = True if find_spec("numpy") else False @@ -86,8 +85,8 @@ def test_frame_palette(): @pytest.mark.skipif(NUMPY_LOADED is False, reason="Numpy not installed") @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") def test_frame_transparency(): - from PIL import Image import numpy + from PIL import Image apngasm = APNGAsmBinder() with Image.open(elephant_frame0_path) as im_rgba: @@ -110,8 +109,8 @@ def test_frame_palette_size(): @pytest.mark.skipif(NUMPY_LOADED is False, reason="Numpy not installed") @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") def test_frame_transparency_size(): - from PIL import Image import numpy + from PIL import Image apngasm = APNGAsmBinder() with Image.open(elephant_frame0_path) as im_rgba: @@ -191,8 +190,8 @@ def test_add_frame_from_pillow_palette(): @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") @pytest.mark.skipif(NUMPY_LOADED is False, reason="Numpy not installed") def test_add_frame_from_numpy_rgba(): - from PIL import Image import numpy + from PIL import Image apngasm = APNGAsmBinder() @@ -205,8 +204,8 @@ def test_add_frame_from_numpy_rgba(): @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") @pytest.mark.skipif(NUMPY_LOADED is False, reason="Numpy not installed") def test_add_frame_from_numpy_rgb(): - from PIL import Image import numpy + from PIL import Image apngasm = APNGAsmBinder() @@ -220,8 +219,8 @@ def test_add_frame_from_numpy_rgb(): @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") @pytest.mark.skipif(NUMPY_LOADED is False, reason="Numpy not installed") def test_add_frame_from_numpy_rgb_trns(): - from PIL import Image import numpy + from PIL import Image apngasm = APNGAsmBinder() @@ -236,8 +235,8 @@ def test_add_frame_from_numpy_rgb_trns(): @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") @pytest.mark.skipif(NUMPY_LOADED is False, reason="Numpy not installed") def test_add_frame_from_numpy_non_rgb(): - from PIL import Image import numpy + from PIL import Image apngasm = APNGAsmBinder() diff --git a/tests/test_direct.py b/tests/test_direct.py index 750a4f0..f589a28 100644 --- a/tests/test_direct.py +++ b/tests/test_direct.py @@ -1,16 +1,12 @@ #!/usr/bin/env python3 -from importlib.util import find_spec import os +from importlib.util import find_spec import pytest from _pytest._py.path import LocalPath - -from apngasm_python._apngasm_python import ( - APNGAsm, - APNGFrame, - create_frame_from_rgb_trns, - create_frame_from_rgba, -) +from apngasm_python._apngasm_python import (APNGAsm, APNGFrame, + create_frame_from_rgb_trns, + create_frame_from_rgba) PILLOW_LOADED = True if find_spec("PIL") else False NUMPY_LOADED = True if find_spec("numpy") else False @@ -33,8 +29,8 @@ @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") @pytest.mark.skipif(NUMPY_LOADED is False, reason="Numpy not installed") def test_frame_pixels(): - from PIL import Image import numpy + from PIL import Image image = Image.open(elephant_frame0_path) frame = create_frame_from_rgba(numpy.array(image), image.width, image.height) @@ -45,8 +41,8 @@ def test_frame_pixels(): @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") @pytest.mark.skipif(NUMPY_LOADED is False, reason="Numpy not installed") def test_frame_width(): - from PIL import Image import numpy + from PIL import Image image = Image.open(elephant_frame0_path) frame = create_frame_from_rgba(numpy.array(image), image.width, image.height) @@ -57,8 +53,8 @@ def test_frame_width(): @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") @pytest.mark.skipif(NUMPY_LOADED is False, reason="Numpy not installed") def test_frame_height(): - from PIL import Image import numpy + from PIL import Image image = Image.open(elephant_frame0_path) frame = create_frame_from_rgba(numpy.array(image), image.width, image.height) @@ -69,8 +65,8 @@ def test_frame_height(): @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") @pytest.mark.skipif(NUMPY_LOADED is False, reason="Numpy not installed") def test_frame_color_type(): - from PIL import Image import numpy + from PIL import Image image = Image.open(elephant_frame0_path) frame = create_frame_from_rgba(numpy.array(image), image.width, image.height) @@ -81,8 +77,8 @@ def test_frame_color_type(): @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") @pytest.mark.skipif(NUMPY_LOADED is False, reason="Numpy not installed") def test_frame_palette(): - from PIL import Image import numpy + from PIL import Image image = Image.open(elephant_frame0_path) frame = create_frame_from_rgba(numpy.array(image), image.width, image.height) @@ -92,8 +88,8 @@ def test_frame_palette(): @pytest.mark.skipif(NUMPY_LOADED is False, reason="Numpy not installed") @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") def test_frame_transparency(): - from PIL import Image import numpy + from PIL import Image image = Image.open(elephant_frame0_path).convert("RGB") frame = create_frame_from_rgb_trns( @@ -106,8 +102,8 @@ def test_frame_transparency(): @pytest.mark.skipif(NUMPY_LOADED is False, reason="Numpy not installed") @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") def test_frame_palette_size(): - from PIL import Image import numpy + from PIL import Image image = Image.open(elephant_frame0_path) frame = create_frame_from_rgba(numpy.array(image), image.width, image.height) @@ -118,8 +114,8 @@ def test_frame_palette_size(): @pytest.mark.skipif(NUMPY_LOADED is False, reason="Numpy not installed") @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") def test_frame_transparency_size(): - from PIL import Image import numpy + from PIL import Image image = Image.open(elephant_frame0_path).convert("RGB") frame = create_frame_from_rgb_trns( @@ -131,8 +127,8 @@ def test_frame_transparency_size(): @pytest.mark.skipif(NUMPY_LOADED is False, reason="Numpy not installed") @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") def test_frame_delay_num(): - from PIL import Image import numpy + from PIL import Image image = Image.open(elephant_frame0_path) frame = create_frame_from_rgba( @@ -144,8 +140,8 @@ def test_frame_delay_num(): @pytest.mark.skipif(NUMPY_LOADED is False, reason="Numpy not installed") @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") def test_frame_delay_den(): - from PIL import Image import numpy + from PIL import Image image = Image.open(elephant_frame0_path) frame = create_frame_from_rgba( From 91011d65abc6f97bf48fa4c5235dd0f101812d8b Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sat, 9 Mar 2024 17:50:23 +0800 Subject: [PATCH 090/103] Specify directories to check for mypy --- .github/workflows/check_and_fix.yml | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check_and_fix.yml b/.github/workflows/check_and_fix.yml index fcdaeb1..7001299 100644 --- a/.github/workflows/check_and_fix.yml +++ b/.github/workflows/check_and_fix.yml @@ -24,14 +24,14 @@ jobs: python-version: '3.9' - name: Install dependency run: | - pip install build ruff mypy Pillow numpy pytest + pip install build ruff mypy Pillow numpy pytest types-Pillow pip install git+https://github.com/wjakob/nanobind.git - name: Ruff check run: ruff check - name: Install test run: pip install . - name: mypy - run: mypy . + run: mypy - name: ruff format run: ruff format - name: isort diff --git a/pyproject.toml b/pyproject.toml index 5ef6409..ddbda4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,7 @@ strict = ["*"] [tool.mypy] python_version = "3.9" -exclude = ["lipo-dir-merge"] +include = ["src-python", "scripts", "tests", "example"] [tool.isort] extend_skip = ["lipo-dir-merge"] From d0c62ea9c967a1ef1d86d302fc1341151bebb983 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sat, 9 Mar 2024 17:54:21 +0800 Subject: [PATCH 091/103] Fix mypy config --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ddbda4a..ae6b8c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,7 @@ strict = ["*"] [tool.mypy] python_version = "3.9" -include = ["src-python", "scripts", "tests", "example"] +files = ["src-python", "scripts", "tests", "example"] [tool.isort] extend_skip = ["lipo-dir-merge"] From 7c99e88f08789348d1503dc5a6189a1c196b9e06 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sat, 9 Mar 2024 17:57:57 +0800 Subject: [PATCH 092/103] Install isort in github action --- .github/workflows/check_and_fix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check_and_fix.yml b/.github/workflows/check_and_fix.yml index 7001299..01c8fc5 100644 --- a/.github/workflows/check_and_fix.yml +++ b/.github/workflows/check_and_fix.yml @@ -24,7 +24,7 @@ jobs: python-version: '3.9' - name: Install dependency run: | - pip install build ruff mypy Pillow numpy pytest types-Pillow + pip install build ruff mypy isort Pillow numpy pytest types-Pillow pip install git+https://github.com/wjakob/nanobind.git - name: Ruff check run: ruff check From 990d744337b231e3f242a9ecff8754f6a6437702 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 9 Mar 2024 10:01:19 +0000 Subject: [PATCH 093/103] Update stub and formatting --- conanfile.py | 14 ++++++++------ src-python/apngasm_python/apngasm.py | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/conanfile.py b/conanfile.py index 740b2aa..c54f345 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # type: ignore -from conan import ConanFile import shutil -from scripts.get_arch import get_arch -from conan.tools.cmake import CMakeToolchain, CMakeDeps + +from conan import ConanFile from conan.tools.apple import is_apple_os +from conan.tools.cmake import CMakeDeps, CMakeToolchain + +from scripts.get_arch import get_arch class ApngasmRecipe(ConanFile): @@ -27,8 +29,8 @@ 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" + tc.blocks["apple_system"].values["cmake_osx_architectures"] = ( + "x86_64; arm64" + ) tc.generate() cmake.generate() diff --git a/src-python/apngasm_python/apngasm.py b/src-python/apngasm_python/apngasm.py index df6131e..527d20d 100755 --- a/src-python/apngasm_python/apngasm.py +++ b/src-python/apngasm_python/apngasm.py @@ -7,8 +7,8 @@ from numpy.typing import NDArray from PIL import Image -from ._apngasm_python import (APNGAsm, APNGFrame, # type: ignore - IAPNGAsmListener, create_frame_from_rgb, +from ._apngasm_python import APNGFrame # type: ignore +from ._apngasm_python import (APNGAsm, IAPNGAsmListener, create_frame_from_rgb, create_frame_from_rgb_trns, create_frame_from_rgba) From 9342fa2744c3abe6485eb23300c163c444913cd2 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sun, 10 Mar 2024 00:25:45 +0800 Subject: [PATCH 094/103] Update project description --- README.md | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 95a885a..a502bd9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # apngasm-python -A nanobind API for [apngasm](https://github.com/apngasm/apngasm), a tool/library for -APNG assembly/disassembly. +A nanobind API for [apngasm](https://github.com/apngasm/apngasm), which is a +tool/library for APNG assembly & disassembly with compression support. apngasm is originally a CLI program for quickly assembling PNG images into animated PNG (APNG). It also supports creating compressed APNG. diff --git a/pyproject.toml b/pyproject.toml index ae6b8c4..8cd7ab5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] # Project metadata name = "apngasm-python" -description = "A nanobind API for apngasm, a tool/library for APNG assembly/disassembly." +description = "A nanobind python API for apngasm, a tool/library for APNG assembly & disassembly with compression support." requires-python = ">=3.8" readme = "README.md" license = { "file" = "LICENSE" } From 1b9b5f70176f1f1d8d71f699ac743453b0efb031 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Tue, 12 Mar 2024 08:34:09 +0800 Subject: [PATCH 095/103] Update cibuildwheel --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a5eb3b3..fcd7bc4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -114,7 +114,7 @@ jobs: platforms: all - name: Build wheels for ${{ matrix.os }} ${{ matrix.cibw_archs }} ${{ matrix.cibw_build }} - uses: pypa/cibuildwheel@edd67e0d266e14537bbf2619e8c6a379ecb038fc + uses: pypa/cibuildwheel@v2.17.0 env: CIBW_BUILD_FRONTEND: build CIBW_ARCHS: ${{ matrix.cibw_archs }} From 44a010f9044eee0f3d6bbaf978cd9b84740ac10c Mon Sep 17 00:00:00 2001 From: laggykiller Date: Thu, 21 Mar 2024 20:47:32 +0800 Subject: [PATCH 096/103] Add optional dependencies --- .github/workflows/build.yml | 3 +-- .github/workflows/check_and_fix.yml | 12 ++++-------- README.md | 5 +++-- pyproject.toml | 20 ++++++++++++++++++-- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fcd7bc4..76f1054 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,8 +26,7 @@ jobs: - name: Test sdist run: | - python -m pip install dist/apngasm_python-*.tar.gz - pip install pytest Pillow numpy && + python -m pip install dist/apngasm_python-*.tar.gz[full,test] pytest - uses: actions/upload-artifact@v3 diff --git a/.github/workflows/check_and_fix.yml b/.github/workflows/check_and_fix.yml index 01c8fc5..555a2a4 100644 --- a/.github/workflows/check_and_fix.yml +++ b/.github/workflows/check_and_fix.yml @@ -22,18 +22,14 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.9' - - name: Install dependency - run: | - pip install build ruff mypy isort Pillow numpy pytest types-Pillow - pip install git+https://github.com/wjakob/nanobind.git + - name: Install test + run: pip install .[full,test,lint] - name: Ruff check run: ruff check - - name: Install test - run: pip install . - - name: mypy - run: mypy - name: ruff format run: ruff format + - name: mypy + run: mypy - name: isort run: isort . - name: nanobind stubgen diff --git a/README.md b/README.md index a502bd9..932ee71 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,10 @@ Documentations: https://apngasm-python.readthedocs.io/en/latest/ pip install apngasm-python ``` -Optionally, you can also install `Pillow` and `numpy` +`Pillow` and `numpy` are optional dependencies. Without them, +some functions are not usable. To also install them: ``` -pip install Pillow numpy +pip install apngasm-python[full] ``` ## Example usage diff --git a/pyproject.toml b/pyproject.toml index 8cd7ab5..2c31bfa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,12 +15,28 @@ Repository = "https://github.com/laggykiller/apngasm-python" Documentation = "https://apngasm-python.readthedocs.io/en/latest/" Tracker = "https://github.com/laggykiller/apngasm-python/issues" +[project.optional-dependencies] +full = [ + "Pillow", + "numpy", +] + +test = [ + "pytest", +] + +lint = [ + "ruff", + "mypy", + "isort", + "types-Pillow", +] + [build-system] # How pip and other frontends should build this project requires = [ "py-build-cmake==0.2.0a12", "nanobind @ git+https://github.com/wjakob/nanobind.git", - "conan>=2.0", - "wheel" + "conan>=2.0" ] build-backend = "py_build_cmake.build" From 4f2ae2edf2ed8e583bb2c29f730c695825b25ec6 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Thu, 21 Mar 2024 21:14:56 +0800 Subject: [PATCH 097/103] Fix check_and_fix; Improve readme --- .github/workflows/check_and_fix.yml | 1 + README.md | 23 +++++++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check_and_fix.yml b/.github/workflows/check_and_fix.yml index 555a2a4..70d715c 100644 --- a/.github/workflows/check_and_fix.yml +++ b/.github/workflows/check_and_fix.yml @@ -34,6 +34,7 @@ jobs: run: isort . - name: nanobind stubgen run: | + pip install "nanobind @ git+https://github.com/wjakob/nanobind.git" python -m nanobind.stubgen \ -m apngasm_python._apngasm_python \ -o src-python/apngasm_python/_apngasm_python.pyi \ diff --git a/README.md b/README.md index 932ee71..7749b5c 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,13 @@ download apngasm. Documentations: https://apngasm-python.readthedocs.io/en/latest/ ## Install -``` +```bash pip install apngasm-python ``` `Pillow` and `numpy` are optional dependencies. Without them, some functions are not usable. To also install them: -``` +```bash pip install apngasm-python[full] ``` @@ -126,9 +126,8 @@ help(_apngasm_python) ## Building from source ```bash -git clone https://github.com/laggykiller/apngasm-python.git +git clone --recursive https://github.com/laggykiller/apngasm-python.git cd apngasm-python -git submodule update --init --recursive # To build wheel python3 -m build . @@ -154,6 +153,22 @@ export APNGASM_COMPILE_TARGET=ppc64le export APNGASM_COMPILE_TARGET=s390x ``` +## Development +To run tests: +```bash +pip install pytest +pytest +``` + +To lint: +```bash +pip install ruff mypy isort +mypy +isort . +ruff check +ruff format +``` + ## Credits - apngasm: https://github.com/apngasm/apngasm - Packaging: https://github.com/tttapa/py-build-cmake From 5c9a14cf5980bac077137b999ee294c7ed24b03b Mon Sep 17 00:00:00 2001 From: laggykiller Date: Thu, 21 Mar 2024 21:17:51 +0800 Subject: [PATCH 098/103] check_and_fix typo --- .github/workflows/check_and_fix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check_and_fix.yml b/.github/workflows/check_and_fix.yml index 70d715c..2ef861b 100644 --- a/.github/workflows/check_and_fix.yml +++ b/.github/workflows/check_and_fix.yml @@ -26,7 +26,7 @@ jobs: run: pip install .[full,test,lint] - name: Ruff check run: ruff check - - name: ruff format + - name: Ruff format run: ruff format - name: mypy run: mypy From 9788231e739f94ec4e9e71d828fa9d7a3016af54 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 21 Mar 2024 13:21:19 +0000 Subject: [PATCH 099/103] Update stub and formatting --- src-python/apngasm_python/_apngasm_python.pyi | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src-python/apngasm_python/_apngasm_python.pyi b/src-python/apngasm_python/_apngasm_python.pyi index e3d99bb..b03b6b2 100644 --- a/src-python/apngasm_python/_apngasm_python.pyi +++ b/src-python/apngasm_python/_apngasm_python.pyi @@ -1,6 +1,8 @@ from collections.abc import Sequence +from typing import Annotated, Optional, overload + from numpy.typing import ArrayLike -from typing import overload, Optional, Annotated + class APNGAsm: """Class representing APNG file, storing APNGFrame(s) and other metadata.""" From 7ecfe39ebe7da8b4ee77d7ffb11898e99e994941 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Thu, 21 Mar 2024 21:43:13 +0800 Subject: [PATCH 100/103] Fix build --- .github/workflows/build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 76f1054..74af6e8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,8 @@ jobs: - name: Test sdist run: | - python -m pip install dist/apngasm_python-*.tar.gz[full,test] + python -m pip install dist/apngasm_python-*.tar.gz + pip install pytest numpy Pillow pytest - uses: actions/upload-artifact@v3 @@ -128,7 +129,7 @@ jobs: - name: abi3audit run: | - pip install abi3audit && + pip install abi3audit abi3audit $(ls ./wheelhouse/*.whl) --debug --verbose - uses: actions/upload-artifact@v3 From d862e6be690b22d4cd73b80e18ec85cf314f323f Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sun, 24 Mar 2024 22:43:28 +0800 Subject: [PATCH 101/103] getting path of cygcheck is unreliable way to determine if inside cygwin --- scripts/get_deps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/get_deps.py b/scripts/get_deps.py index 12ae2cf..ebd31f7 100755 --- a/scripts/get_deps.py +++ b/scripts/get_deps.py @@ -16,7 +16,7 @@ def install_deps(arch: str): if platform.system() == "Windows": settings.append("os=Windows") - if sys.platform.startswith(("cygwin", "msys")) or shutil.which("cygcheck"): + if sys.platform.startswith(("cygwin", "msys")): # Need python headers and libraries, but msvc not able to find them # If inside cygwin or msys. settings.append("compiler=gcc") From e522995ba83ceebc4b093547116b203931a44334 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Fri, 24 May 2024 07:09:53 +0800 Subject: [PATCH 102/103] Update nanobind to latest release --- .github/workflows/check_and_fix.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check_and_fix.yml b/.github/workflows/check_and_fix.yml index 2ef861b..ba0b30e 100644 --- a/.github/workflows/check_and_fix.yml +++ b/.github/workflows/check_and_fix.yml @@ -34,7 +34,7 @@ jobs: run: isort . - name: nanobind stubgen run: | - pip install "nanobind @ git+https://github.com/wjakob/nanobind.git" + pip install nanobind python -m nanobind.stubgen \ -m apngasm_python._apngasm_python \ -o src-python/apngasm_python/_apngasm_python.pyi \ diff --git a/pyproject.toml b/pyproject.toml index 2c31bfa..57f0196 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ lint = [ [build-system] # How pip and other frontends should build this project requires = [ "py-build-cmake==0.2.0a12", - "nanobind @ git+https://github.com/wjakob/nanobind.git", + "nanobind>=2.0.0", "conan>=2.0" ] build-backend = "py_build_cmake.build" From 0d655516f70511e432d9c8f3339851d5824b2fd1 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Fri, 24 May 2024 07:48:55 +0800 Subject: [PATCH 103/103] Adapt to new nanobind 2.0.0 ndarray constructor signature --- src/apngasm_python.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/apngasm_python.cpp b/src/apngasm_python.cpp index 30e2122..52f6e8e 100644 --- a/src/apngasm_python.cpp +++ b/src/apngasm_python.cpp @@ -257,7 +257,7 @@ NB_MODULE(MODULE_NAME, m) { [](apngasm::APNGFrame &t) APNGASM_PY_DECLSPEC { size_t rowbytes = rowbytesMap[t._colorType]; size_t shape[3] = { t._height, t._width, rowbytes }; - return nb::ndarray>(t._pixels, 3, shape); + return nb::ndarray>(t._pixels, 3, shape, nb::handle()); }, [](apngasm::APNGFrame &t, nb::ndarray> *v) APNGASM_PY_DECLSPEC { size_t rowbytes = rowbytesMap[t._colorType]; @@ -316,7 +316,7 @@ NB_MODULE(MODULE_NAME, m) { paletteView[i][2] = t._palette[i].b; } size_t shape[2] = { 256, 3 }; - return nb::ndarray>(paletteView, 2, shape); + return nb::ndarray>(paletteView, 2, shape, nb::handle()); }, [](apngasm::APNGFrame &t, nb::ndarray> *v) APNGASM_PY_DECLSPEC { unsigned char *v_ptr = v->data(); @@ -335,7 +335,7 @@ NB_MODULE(MODULE_NAME, m) { .def_prop_rw("transparency", [](apngasm::APNGFrame &t) APNGASM_PY_DECLSPEC { size_t shape[1] = { static_cast(t._transparencySize) }; - return nb::ndarray>(t._transparency, 1, shape); + return nb::ndarray>(t._transparency, 1, shape, nb::handle()); }, [](apngasm::APNGFrame &t, nb::ndarray> *v) APNGASM_PY_DECLSPEC { unsigned char *v_ptr = v->data();