├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ └── check_and_fix.yml ├── .gitignore ├── .gitmodules ├── .readthedocs.yaml ├── CMakeLists.txt ├── LICENSE ├── README.md ├── docs ├── Makefile ├── comparing.md ├── conf.py ├── credits.md ├── examples.md ├── index.rst ├── installing.md ├── make.bat ├── modules.rst ├── requirements.txt └── rlottie_python.rst ├── example └── example.py ├── pyproject.toml ├── samples ├── sample.json └── sample.tgs ├── src └── rlottie_python │ ├── __init__.py │ ├── _rlottiecommon.py │ ├── py.typed │ └── rlottie_wrapper.py └── tests └── test_all.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: laggykiller -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and upload to PyPI 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | build_sdist: 10 | name: Build source distribution 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | submodules: recursive 16 | 17 | - name: Build sdist 18 | run: pipx run build --sdist 19 | 20 | - name: Test sdist 21 | run: | 22 | python -m pip install dist/rlottie_python-*.tar.gz 23 | pip install Pillow pytest 24 | pytest 25 | 26 | - uses: actions/upload-artifact@v4 27 | with: 28 | name: sdist 29 | path: dist/*.tar.gz 30 | retention-days: 7 31 | 32 | build_wheels: 33 | name: Build wheels on ${{ matrix.os }} ${{ matrix.arch }} 34 | runs-on: ${{ matrix.os }} 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | include: 39 | - os: windows-2019 40 | arch: x64 41 | cibw_archs_windows: AMD64 42 | cibw_build: "cp37* pp*" 43 | - os: windows-2019 44 | arch: x86 45 | cibw_archs_windows: x86 46 | cibw_build: "cp37*" 47 | - os: windows-2019 48 | arch: arm64 49 | cibw_archs_windows: ARM64 50 | cibw_build: "cp39*" 51 | - os: macos-13 52 | arch: x64 53 | cibw_archs_macos: x86_64 54 | cibw_build: "cp37* pp*" 55 | - os: macos-14 56 | arch: universal2 57 | cibw_archs_macos: universal2 58 | cibw_build: "cp38* pp*" 59 | - os: macos-14 60 | arch: arm64 61 | cibw_archs_macos: arm64 62 | cibw_build: "cp38* pp*" 63 | - os: ubuntu-22.04 64 | arch: x64 65 | cibw_archs_linux: x86_64 66 | cibw_build: "cp37* pp*" 67 | - os: ubuntu-22.04 68 | arch: x86 69 | cibw_archs_linux: i686 70 | cibw_build: "cp37* pp*" 71 | - os: ubuntu-22.04-arm 72 | arch: arm64 73 | cibw_archs_linux: aarch64 74 | cibw_build: "cp37* pp*" 75 | - os: ubuntu-22.04 76 | arch: ppc64le 77 | cibw_archs_linux: ppc64le 78 | cibw_build: "cp37*" 79 | - os: ubuntu-22.04 80 | arch: s390x 81 | cibw_archs_linux: s390x 82 | cibw_build: "cp37*" 83 | 84 | steps: 85 | - uses: actions/setup-python@v5 86 | with: 87 | python-version: 3.11 88 | 89 | - uses: actions/checkout@v3 90 | with: 91 | submodules: recursive 92 | 93 | - name: Set up QEMU 94 | if: runner.os == 'Linux' 95 | uses: docker/setup-qemu-action@v2 96 | with: 97 | platforms: all 98 | 99 | - name: Build wheels 100 | uses: pypa/cibuildwheel@v2.17.0 101 | env: 102 | CIBW_BUILD_FRONTEND: build 103 | CIBW_ARCHS_WINDOWS: ${{ matrix.cibw_archs_windows }} 104 | CIBW_ARCHS_MACOS: ${{ matrix.cibw_archs_macos }} 105 | CIBW_ARCHS_LINUX: ${{ matrix.cibw_archs_linux }} 106 | CIBW_BUILD: ${{ matrix.cibw_build }} 107 | CIBW_TEST_REQUIRES: pytest 108 | CIBW_BEFORE_TEST: pip install --only-binary ":all:" Pillow; true 109 | CIBW_BEFORE_TEST_WINDOWS: pip install --only-binary ":all:" Pillow || VER>NUL 110 | CIBW_TEST_COMMAND: pytest {package}/tests 111 | # Weird bug in pp38-win_amd64 causing test to fail 112 | # OSError: [WinError 6] The handle is invalid: 'D:\\System Volume Information' 113 | CIBW_TEST_SKIP: "pp*-win_amd64" 114 | 115 | - name: abi3audit 116 | run: | 117 | pip install abi3audit 118 | abi3audit $(ls ./wheelhouse/*.whl) --debug --verbose 119 | 120 | - uses: actions/upload-artifact@v4 121 | with: 122 | name: wheels-${{ matrix.os }}-${{ matrix.arch }} 123 | path: ./wheelhouse/*.whl 124 | retention-days: 7 125 | 126 | upload_pypi_test: 127 | needs: [build_wheels, build_sdist] 128 | runs-on: ubuntu-latest 129 | # upload to PyPI on every tag starting with 'v' 130 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') 131 | steps: 132 | - name: "Download artifacts" 133 | uses: actions/download-artifact@v4 134 | 135 | - name: "Move packages to the dist/ folder" 136 | run: | 137 | mkdir dist/ 138 | mv sdist/* dist/ 139 | mv wheels-*/*.whl dist/ 140 | 141 | - name: "Publish packages on PyPI" 142 | uses: pypa/gh-action-pypi-publish@v1.5.0 143 | with: 144 | password: ${{ secrets.PYPI_API_TOKEN }} 145 | repository_url: https://test.pypi.org/legacy/ 146 | 147 | upload_pypi: 148 | needs: [build_wheels, build_sdist] 149 | runs-on: ubuntu-latest 150 | if: github.event_name == 'release' && github.event.action == 'published' 151 | steps: 152 | - name: "Download artifacts" 153 | uses: actions/download-artifact@v4 154 | 155 | - name: "Move packages to the dist/ folder" 156 | run: | 157 | mkdir dist/ 158 | mv sdist/* dist/ 159 | mv wheels-*/*.whl dist/ 160 | 161 | - name: "Publish packages on PyPI" 162 | uses: pypa/gh-action-pypi-publish@v1.5.0 163 | with: 164 | password: ${{ secrets.PYPI_API_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/check_and_fix.yml: -------------------------------------------------------------------------------- 1 | name: Check and formatting 2 | 3 | on: 4 | push: 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | check_and_fix: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@master 15 | with: 16 | fetch-depth: 0 # otherwise, you will failed to push refs to dest repo 17 | submodules: recursive 18 | - name: Extract branch name 19 | shell: bash 20 | run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT 21 | id: extract_branch 22 | - uses: actions/setup-python@v5 23 | with: 24 | python-version: '3.8' 25 | - name: Install test 26 | run: pip install .[full,test,lint] 27 | - name: mypy 28 | run: mypy 29 | - name: isort 30 | run: isort . 31 | - name: Ruff check 32 | run: ruff check 33 | - name: Ruff format 34 | run: ruff format 35 | - name: Commit & Push changes 36 | uses: actions-js/push@master 37 | with: 38 | github_token: ${{ secrets.GITHUB_TOKEN }} 39 | message: Formatting 40 | branch: ${{ steps.extract_branch.outputs.branch }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__ 2 | dist/ 3 | .py-build-cmake_cache/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "rlottie"] 2 | path = rlottie 3 | url = https://github.com/laggykiller/rlottie.git 4 | branch = arm_compat -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the OS, Python version and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | # You can also specify other tool versions: 14 | # nodejs: "19" 15 | # rust: "1.64" 16 | # golang: "1.19" 17 | 18 | # Build documentation in the "docs/" directory with Sphinx 19 | sphinx: 20 | configuration: docs/conf.py 21 | 22 | # Optionally build your docs in additional formats such as PDF and ePub 23 | # formats: 24 | # - pdf 25 | # - epub 26 | 27 | # Optional but recommended, declare the Python requirements required 28 | # to build your documentation 29 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 30 | python: 31 | install: 32 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.17) 2 | project(rlottie-python) 3 | 4 | if (MSVC) 5 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") 6 | set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT") 7 | set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd") 8 | elseif (LINUX) 9 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") 10 | endif() 11 | 12 | add_subdirectory(rlottie) 13 | 14 | # Install the module 15 | if (WIN32) 16 | install(TARGETS rlottie 17 | EXCLUDE_FROM_ALL 18 | RUNTIME DESTINATION ${PY_BUILD_CMAKE_MODULE_NAME} 19 | COMPONENT python_module) 20 | else() 21 | install(TARGETS rlottie 22 | EXCLUDE_FROM_ALL 23 | LIBRARY DESTINATION ${PY_BUILD_CMAKE_MODULE_NAME} 24 | COMPONENT python_module) 25 | endif() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 489 | USA 490 | 491 | Also add information on how to contact you by electronic and paper mail. 492 | 493 | You should also get your employer (if you work as a programmer) or your 494 | school, if any, to sign a "copyright disclaimer" for the library, if 495 | necessary. Here is a sample; alter the names: 496 | 497 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 498 | library `Frob' (a library for tweaking knobs) written by James Random 499 | Hacker. 500 | 501 | , 1 April 1990 502 | Ty Coon, President of Vice 503 | 504 | That's all there is to it! 505 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rlottie-python 2 | 3 | A ctypes API for rlottie, with additional functions for getting Pillow Image and animated sequences, as well as telegram animated stickers (tgs). 4 | 5 | See example/example.py for example usage. 6 | 7 | The functions mostly follow [rlottie/inc/rlottie_capi.h](https://github.com/Samsung/rlottie/blob/master/inc/rlottie_capi.h) 8 | 9 | Documentations: https://rlottie-python.readthedocs.io/en/latest/ 10 | 11 | ## Table of contents 12 | - [Installing](#installing) 13 | - [Building from source](#building-from-source) 14 | - [Examples](#examples) 15 | - [Comparing to other library](#comparing-to-other-library) 16 | - [Credits](#credits) 17 | 18 | ## Installing 19 | 20 | Note that rlottie is included in the wheel package, you need not install librlottie. 21 | 22 | To install, run the following: 23 | ```bash 24 | pip3 install rlottie-python 25 | ``` 26 | 27 | `Pillow` is optional dependency. It is required for `render_pillow_frame()`, 28 | `save_frame()` and `save_animation()`. To also install Pillow, run: 29 | ```bash 30 | pip3 install rlottie-python[full] 31 | ``` 32 | 33 | ## Examples 34 | Getting information about an lottie animation 35 | ```python 36 | from rlottie_python import LottieAnimation 37 | 38 | anim = LottieAnimation.from_file("samples/sample.json") 39 | frames = anim.lottie_animation_get_totalframe() 40 | print(f"{frames = }") 41 | 42 | width, height = anim.lottie_animation_get_size() 43 | print(f"{width, height = }") 44 | 45 | duration = anim.lottie_animation_get_duration() 46 | print(f"{duration = }") 47 | 48 | totalframe = anim.lottie_animation_get_totalframe() 49 | print(f"{totalframe = }") 50 | 51 | framerate = anim.lottie_animation_get_framerate() 52 | print(f"{framerate = }") 53 | 54 | render_tree = anim.lottie_animation_render_tree(0) 55 | print(f"{render_tree.mMaskList.size = }") 56 | 57 | mapped_frame = anim.lottie_animation_get_frame_at_pos(0) 58 | print(f"{mapped_frame = }") 59 | ``` 60 | 61 | Rendering and saving frame 62 | ```python 63 | from rlottie_python import LottieAnimation 64 | from PIL import Image 65 | 66 | anim = LottieAnimation.from_file("samples/sample.json") 67 | 68 | # Method 1: Saving the frame to file directly 69 | anim.save_frame("frame30.png", frame_num=30) 70 | 71 | # Method 2: Getting Pillow Image 72 | im = anim.render_pillow_frame(frame_num=40) 73 | im.save("frame40.png") 74 | 75 | # Method 3: Getting buffer 76 | buffer = anim.lottie_animation_render(frame_num=50) 77 | width, height = anim.lottie_animation_get_size() 78 | im = Image.frombuffer("RGBA", (width, height), buffer, "raw", "BGRA") 79 | im.save("frame50.png") 80 | ``` 81 | 82 | Loading from JSON file, string of JSON, tgs; and rendering animation 83 | ```python 84 | from rlottie_python import LottieAnimation 85 | 86 | # Loading from file 87 | anim = LottieAnimation.from_file("samples/sample.json") 88 | anim.save_animation("animation1.apng") 89 | 90 | anim = LottieAnimation.from_tgs("samples/sample.tgs") 91 | anim.save_animation("animation2.gif") 92 | 93 | with open("samples/sample.json", encoding="utf-8") as f: 94 | data = f.read() 95 | 96 | anim = LottieAnimation.from_data(data=data) 97 | anim.save_animation("animation3.webp") 98 | ``` 99 | 100 | You may also load animation using with statement 101 | ```python 102 | from rlottie_python import LottieAnimation 103 | 104 | with LottieAnimation.from_file("samples/sample.json") as anim: 105 | anim.save_animation("animation4.apng") 106 | ``` 107 | 108 | Notice, if you are running on Linux and want to use rlottie_python in main process 109 | and child processes spawned by `multiprocessing.Process`, you may have to change 110 | start method to `spawn`, or else deadlock may occur: 111 | ```python 112 | if __name__ == "__main__": 113 | multiprocessing.set_start_method("spawn") 114 | ``` 115 | 116 | ## Comparing to other library 117 | The `lottie` (https://pypi.org/project/lottie/) python package is also capable of working with lottie files and telegram animated stickers (tgs). It is also able to support many input/output formats and vector graphics, without any dependency on extenral libraries such as librlottie. However some images it creates is broken ([Example1](https://github.com/laggykiller/sticker-convert/issues/5) [Example2](https://gitlab.com/mattbas/python-lottie/-/issues/95)). It seems librlottie is more stable in terms of rendering frames. 118 | 119 | The `pyrlottie` (https://pypi.org/project/pyrlottie/) python package is also able to convert lottie and tgs files to webp/gif. However, it works by calling executables `gif2webp` and `lottie2gif` with subprocess, and it does not support macOS. 120 | 121 | ## Building from source 122 | 123 | To build wheel, run the following: 124 | ```bash 125 | git clone --recursive https://github.com/laggykiller/rlottie-python.git 126 | cd rlottie-python 127 | 128 | # To build wheel 129 | python3 -m build . 130 | 131 | # To install directly 132 | pip3 install . 133 | ``` 134 | 135 | ## Development 136 | To run tests: 137 | ```bash 138 | pip install pytest 139 | pytest 140 | ``` 141 | 142 | To lint: 143 | ```bash 144 | pip install ruff mypy isort 145 | mypy 146 | isort . 147 | ruff check 148 | ruff format 149 | ``` 150 | 151 | ## Credits 152 | - rlottie library: https://github.com/Samsung/rlottie 153 | - Packaging: https://github.com/tttapa/py-build-cmake -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/comparing.md: -------------------------------------------------------------------------------- 1 | # Compare to lottie 2 | The `lottie` (https://pypi.org/project/lottie/) python package is also capable of working with lottie files and telegram animated stickers (tgs). It is also able to support many input/output formats and vector graphics, without any dependency on extenral libraries such as librlottie. However some images it creates is broken ([Example1](https://github.com/laggykiller/sticker-convert/issues/5) [Example2](https://gitlab.com/mattbas/python-lottie/-/issues/95)). It seems librlottie is more stable in terms of rendering frames. 3 | 4 | # Compare to pyrlottie 5 | The `pyrlottie` (https://pypi.org/project/pyrlottie/) python package is also able to convert lottie and tgs files to webp/gif. However, it works by calling executables `gif2webp` and `lottie2gif` with subprocess, and it does not support macOS. -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | import os 9 | import sys 10 | 11 | sys.path.insert(0, os.path.abspath("../src")) 12 | import rlottie_python # type: ignore # noqa: F401 13 | 14 | project = "rlottie-python" 15 | copyright = "2023, laggykiller" 16 | author = "laggykiller" 17 | 18 | # -- General configuration --------------------------------------------------- 19 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 20 | 21 | extensions = [ 22 | "sphinx.ext.autodoc", 23 | "sphinx.ext.mathjax", 24 | "sphinx.ext.napoleon", 25 | "myst_parser", 26 | "sphinx_immaterial", 27 | ] 28 | 29 | napoleon_google_docstring = False 30 | napoleon_numpy_docstring = True 31 | napoleon_use_ivar = True 32 | 33 | templates_path = ["_templates"] 34 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 35 | 36 | # -- Options for HTML output ------------------------------------------------- 37 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 38 | 39 | html_theme = "sphinx_immaterial" 40 | html_static_path = ["_static"] 41 | # material theme options (see theme.conf for more information) 42 | html_theme_options = { 43 | "repo_name": "rlottie-python", 44 | "globaltoc_collapse": True, 45 | "features": [ 46 | "navigation.expand", 47 | # "navigation.tabs", 48 | # "toc.integrate", 49 | "navigation.sections", 50 | # "navigation.instant", 51 | # "header.autohide", 52 | "navigation.top", 53 | # "navigation.tracking", 54 | # "search.highlight", 55 | "search.share", 56 | "toc.follow", 57 | "toc.sticky", 58 | "content.tabs.link", 59 | "announce.dismiss", 60 | ], 61 | "palette": [ 62 | { 63 | "media": "(prefers-color-scheme: light)", 64 | "scheme": "default", 65 | "primary": "light-green", 66 | "accent": "light-blue", 67 | "toggle": { 68 | "icon": "material/lightbulb-outline", 69 | "name": "Switch to dark mode", 70 | }, 71 | }, 72 | { 73 | "media": "(prefers-color-scheme: dark)", 74 | "scheme": "slate", 75 | "primary": "deep-orange", 76 | "accent": "lime", 77 | "toggle": { 78 | "icon": "material/lightbulb", 79 | "name": "Switch to light mode", 80 | }, 81 | }, 82 | ], 83 | "toc_title_is_page_title": True, 84 | } 85 | -------------------------------------------------------------------------------- /docs/credits.md: -------------------------------------------------------------------------------- 1 | # Credits 2 | 3 | - rlottie library: https://github.com/Samsung/rlottie 4 | - Packaging: https://github.com/tttapa/py-build-cmake -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Getting information about an lottie animation 4 | ```python 5 | from rlottie_python import LottieAnimation 6 | 7 | anim = LottieAnimation.from_file('example/sample.json') 8 | frames = anim.lottie_animation_get_totalframe() 9 | print(f'{frames = }') 10 | 11 | width, height = anim.lottie_animation_get_size() 12 | print(f'{width, height = }') 13 | 14 | duration = anim.lottie_animation_get_duration() 15 | print(f'{duration = }') 16 | 17 | totalframe = anim.lottie_animation_get_totalframe() 18 | print(f'{totalframe = }') 19 | 20 | framerate = anim.lottie_animation_get_framerate() 21 | print(f'{framerate = }') 22 | 23 | render_tree = anim.lottie_animation_render_tree(0) 24 | print(f'{render_tree.mMaskList.size = }') 25 | 26 | mapped_frame = anim.lottie_animation_get_frame_at_pos(0) 27 | print(f'{mapped_frame = }') 28 | ``` 29 | 30 | Rendering and saving frame 31 | ```python 32 | from rlottie_python import LottieAnimation 33 | from PIL import Image 34 | 35 | anim = LottieAnimation.from_file('example/sample.json') 36 | 37 | # Method 1: Saving the frame to file directly 38 | anim.save_frame('frame30.png', frame_num=30) 39 | 40 | # Method 2: Getting Pillow Image 41 | im = anim.render_pillow_frame(frame_num=40) 42 | im.save('frame40.png') 43 | 44 | # Method 3: Getting buffer 45 | buffer = anim.lottie_animation_render(frame_num=50) 46 | width, height = anim.lottie_animation_get_size() 47 | im = Image.frombuffer('RGBA', (width, height), buffer, 'raw', 'BGRA') 48 | im.save('frame50.png') 49 | ``` 50 | 51 | Loading from JSON file, string of JSON, tgs; and rendering animation 52 | ```python 53 | from rlottie_python import LottieAnimation 54 | 55 | # Loading from file 56 | anim = LottieAnimation.from_file('example/sample.json') 57 | anim.save_animation('animation1.apng') 58 | 59 | anim = LottieAnimation.from_tgs('example/sample.tgs') 60 | anim.save_animation('animation2.gif') 61 | 62 | with open('example/sample.json') as f: 63 | data = f.read() 64 | 65 | anim = LottieAnimation.from_data(data=data) 66 | anim.save_animation('animation3.webp') 67 | ``` 68 | 69 | You may also load animation using with statement 70 | ```python 71 | from rlottie_python import LottieAnimation 72 | 73 | with LottieAnimation.from_file('example/sample.json') as anim: 74 | anim.save_animation('animation4.apng') -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. rlottie-python documentation master file, created by 2 | sphinx-quickstart on Fri Sep 8 18:35:57 2023. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to rlottie-python's documentation! 7 | ========================================== 8 | 9 | rlottie-python is a ctypes API for rlottie, with additional functions for getting Pillow Image and animated sequences, as well as telegram animated stickers (tgs). 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | :caption: Contents: 14 | 15 | installing 16 | comparing 17 | examples 18 | credits 19 | modules -------------------------------------------------------------------------------- /docs/installing.md: -------------------------------------------------------------------------------- 1 | Installing 2 | ============== 3 | 4 | ## Insalling from pip 5 | Note that rlottie is included in the wheel package, you need not install librlottie. 6 | 7 | To install, run the following: 8 | ``` 9 | pip3 install wheel 10 | pip3 install rlottie-python 11 | ``` 12 | 13 | ## Building from source 14 | 15 | To build wheel, run the following: 16 | ``` 17 | git clone --recursive https://github.com/laggykiller/rlottie-python.git 18 | cd rlottie-python 19 | pip3 install -r requirements.txt 20 | python3 -m build . 21 | ``` 22 | 23 | To install the built wheel, run `pip3 install dist/.whl` 24 | 25 | If you want to install directly, run the following: 26 | ```bash 27 | git clone --recursive https://github.com/laggykiller/rlottie-python.git 28 | cd rlottie-python 29 | pip3 install -r requirements.txt 30 | pip3 install . 31 | ``` -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | API references 2 | ============== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | rlottie_python 8 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow 2 | myst-parser 3 | sphinx_immaterial -------------------------------------------------------------------------------- /docs/rlottie_python.rst: -------------------------------------------------------------------------------- 1 | rlottie\_python package 2 | ======================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | rlottie\_python.rlottie\_wrapper module 8 | --------------------------------------- 9 | 10 | .. automodule:: rlottie_python.rlottie_wrapper 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | .. rlottie\_python.rlottiecommon module 16 | .. ------------------------------------ 17 | 18 | .. .. automodule:: rlottie_python.rlottiecommon 19 | .. :members: 20 | .. :undoc-members: 21 | .. :show-inheritance: 22 | 23 | Module contents 24 | --------------- 25 | 26 | .. automodule:: rlottie_python 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | -------------------------------------------------------------------------------- /example/example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | 4 | from PIL import Image 5 | 6 | from rlottie_python import LottieAnimation 7 | 8 | file_dir = os.path.split(__file__)[0] 9 | json_file = os.path.join(file_dir, "../samples/sample.json") 10 | tgs_file = os.path.join(file_dir, "../samples/sample.tgs") 11 | 12 | # Loading from json file 13 | anim = LottieAnimation.from_file(json_file) 14 | 15 | # Getting attributes about the json file 16 | frames = anim.lottie_animation_get_totalframe() 17 | print(f"{frames = }") 18 | 19 | width, height = anim.lottie_animation_get_size() 20 | print(f"{width, height = }") 21 | 22 | duration = anim.lottie_animation_get_duration() 23 | print(f"{duration = }") 24 | 25 | totalframe = anim.lottie_animation_get_totalframe() 26 | print(f"{totalframe = }") 27 | 28 | framerate = anim.lottie_animation_get_framerate() 29 | print(f"{framerate = }") 30 | 31 | render_tree = anim.lottie_animation_render_tree(0) 32 | print(f"{render_tree.mMaskList.size = }") 33 | 34 | mapped_frame = anim.lottie_animation_get_frame_at_pos(0) 35 | print(f"{mapped_frame = }") 36 | 37 | # Saving frame manually 38 | buffer = anim.lottie_animation_render(frame_num=30) 39 | im = Image.frombuffer("RGBA", (width, height), buffer, "raw", "BGRA") # type: ignore 40 | im.save("test1.png") 41 | 42 | # Loading from tgs file 43 | anim = LottieAnimation.from_tgs(path=tgs_file) 44 | 45 | # Directly get pillow Image 46 | im = anim.render_pillow_frame(frame_num=40) 47 | im.save("test2.png") 48 | 49 | # Directly get buffer 50 | buffer = anim.lottie_animation_render(frame_num=50) 51 | width, height = anim.lottie_animation_get_size() 52 | im = Image.frombuffer("RGBA", (width, height), buffer, "raw", "BGRA") # type: ignore 53 | im.save("test3.png") 54 | 55 | # Directly get buffer, async 56 | anim.lottie_animation_render_async(frame_num=50) 57 | # You may do other other tasks in between 58 | buffer = anim.lottie_animation_render_flush() 59 | width, height = anim.lottie_animation_get_size() 60 | im = Image.frombuffer("RGBA", (width, height), buffer, "raw", "BGRA") # type: ignore 61 | im.save("test4.png") 62 | 63 | # Loading JSON string with from_data() 64 | with open(json_file, encoding="utf-8") as f: 65 | data = f.read() 66 | 67 | # Alternative way of creating instance of LottieAnimation 68 | with LottieAnimation.from_data(data=data) as anim: 69 | # Saving frame with save_frame 70 | anim.save_frame("test5.png", frame_num=30) 71 | 72 | # Saving animation with save_animation 73 | with LottieAnimation.from_tgs(path=tgs_file) as anim: 74 | anim.save_animation("test6.apng") 75 | anim.save_animation("test7.gif") 76 | anim.save_animation("test8.webp") 77 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] # Project metadata 2 | name = "rlottie-python" 3 | description = "A ctypes API for rlottie, with additional functions for getting Pillow Image." 4 | requires-python = ">=3.7" 5 | readme = "README.md" 6 | license = { "file" = "LICENSE" } 7 | authors = [{ "name" = "chaudominic", "email" = "chaudominic2@gmail.com" }] 8 | keywords = ["rlottie", "lottie", "tgs", "ctypes"] 9 | classifiers = ["Topic :: Multimedia :: Graphics"] 10 | dependencies = [] 11 | dynamic = ["version"] 12 | 13 | [project.urls] 14 | Repository = "https://github.com/laggykiller/rlottie-python" 15 | Documentation = "https://rlottie-python.readthedocs.io/en/latest/" 16 | Tracker = "https://github.com/laggykiller/rlottie-python/issues" 17 | 18 | [project.optional-dependencies] 19 | full = [ 20 | "Pillow", 21 | ] 22 | 23 | test = [ 24 | "pytest" 25 | ] 26 | 27 | lint = [ 28 | "ruff", 29 | "mypy", 30 | "isort", 31 | "types-Pillow", 32 | ] 33 | 34 | [build-system] # How pip and other frontends should build this project 35 | requires = ["py-build-cmake>=0.4.3"] 36 | build-backend = "py_build_cmake.build" 37 | 38 | [tool.py-build-cmake.module] # Where to find the Python module to package 39 | name = "rlottie_python" 40 | directory = "src" 41 | 42 | [tool.py-build-cmake.sdist] # What to include in source distributions 43 | include = ["CMakeLists.txt", "rlottie/*"] 44 | 45 | [tool.py-build-cmake.cmake] # How to build the CMake project 46 | build_type = "Release" 47 | source_path = "." 48 | build_args = ["-j"] 49 | options = {"LOTTIE_MODULE" = "OFF", "CMAKE_POLICY_VERSION_MINIMUM" = "3.5"} 50 | install_components = ["python_module"] 51 | 52 | [tool.py-build-cmake.linux.cmake] # Linux-specific options 53 | config = "Release" 54 | env = { "CMAKE_PREFIX_PATH" = "${HOME}/.local" } 55 | 56 | [tool.py-build-cmake.mac.cmake] # macOS-specific options 57 | config = "Release" 58 | 59 | [tool.py-build-cmake.windows.cmake] # Windows-specific options 60 | config = "Release" 61 | 62 | [tool.py-build-cmake.wheel] 63 | python_abi = 'abi3' 64 | abi3_minimum_cpython_version = 37 65 | 66 | [tool.pyright] 67 | include = ["src", "tests", "example"] 68 | strict = ["*"] 69 | 70 | [tool.mypy] 71 | python_version = "3.9" 72 | files = ["src", "tests", "example"] -------------------------------------------------------------------------------- /samples/sample.json: -------------------------------------------------------------------------------- 1 | {"assets":[],"layers":[{"ddd":0,"ind":0,"ty":1,"nm":"品蓝色 纯色 1","ks":{"o":{"k":[{"i":{"x":[0.667],"y":[0.667]},"o":{"x":[0.167],"y":[0.167]},"n":["0p667_0p667_0p167_0p167"],"t":0,"s":[100],"e":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.537],"y":[0]},"n":["0p667_1_0p537_0"],"t":5,"s":[100],"e":[0]},{"t":17}]},"r":{"k":0},"p":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":5,"s":[400,320,0],"e":[408,274,0],"to":[1.33333337306976,-7.66666650772095,0],"ti":[-1.33333337306976,7.66666650772095,0]},{"t":17}]},"a":{"k":[400,300,0]},"s":{"k":[{"i":{"x":[0.518,0.518,0.667],"y":[1,1,0.667]},"o":{"x":[0.16,0.16,0.333],"y":[0.329,0.329,0.333]},"n":["0p518_1_0p16_0p329","0p518_1_0p16_0p329","0p667_0p667_0p333_0p333"],"t":5,"s":[0,0,100],"e":[160,160,100]},{"t":17}]}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"f","pt":{"k":{"i":[[0,0],[3.675,0],[0,0],[2.623,1.039],[10.49,18.686],[8.393,0],[0,0],[5.767,-5.706],[-0.526,-6.747],[8.391,-4.158],[2.101,0],[0,0],[0,-4.152],[0,0],[-4.196,0],[0,0],[-1.574,1.04],[-1.051,-0.52],[-31.47,0],[-9.441,41.527],[-2.097,6.748],[5.768,7.267]],"o":[[-7.342,-8.823],[0,0],[-12.065,0],[14.688,-23.358],[-5.772,-9.866],[0,0],[-3.671,-0.52],[-3.672,3.633],[4.198,46.715],[-1.572,-1.556],[0,0],[-4.198,0],[0,0],[0,4.155],[0,0],[2.097,0],[0.525,1.036],[0,0],[31.47,0],[2.098,-9.345],[3.671,-14.014],[0,0]],"v":[[521.158,276.64],[498.604,267.817],[454.021,267.817],[433.565,265.741],[435.663,188.92],[414.159,173.864],[413.112,173.864],[395.279,178.534],[390.561,194.626],[337.06,270.932],[331.816,268.856],[280.411,268.856],[272.544,276.642],[272.544,419.386],[280.411,427.172],[332.339,427.172],[338.111,425.096],[340.208,427.172],[464.514,426.652],[517.49,333.736],[523.785,308.82],[521.16,276.637]],"c":true}},"o":{"k":100},"x":{"k":0},"nm":"蒙版 1"}],"sw":800,"sh":600,"sc":"#00b1ff","ip":0,"op":50,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":1,"ty":1,"nm":"品蓝色 纯色 3","ks":{"o":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":11,"s":[0],"e":[100]},{"t":15}]},"r":{"k":0},"p":{"k":[400,300,0]},"a":{"k":[400,300,0]},"s":{"k":[100,100,100]}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"k":{"i":[[0,0],[3.675,0],[0,0],[2.623,1.039],[10.49,18.686],[8.393,0],[0,0],[5.767,-5.706],[-0.526,-6.747],[8.391,-4.158],[2.101,0],[0,0],[0,-4.152],[0,0],[-4.196,0],[0,0],[-1.574,1.04],[-1.051,-0.52],[-31.47,0],[-9.441,41.527],[-2.097,6.748],[5.768,7.267]],"o":[[-7.342,-8.823],[0,0],[-12.065,0],[14.688,-23.358],[-5.772,-9.866],[0,0],[-3.671,-0.52],[-3.672,3.633],[4.198,46.715],[-1.572,-1.556],[0,0],[-4.198,0],[0,0],[0,4.155],[0,0],[2.097,0],[0.525,1.036],[0,0],[31.47,0],[2.098,-9.345],[3.671,-14.014],[0,0]],"v":[[521.158,276.64],[498.604,267.817],[454.021,267.817],[433.565,265.741],[435.663,188.92],[414.159,173.864],[413.112,173.864],[395.279,178.534],[390.561,194.626],[337.06,270.932],[331.816,268.856],[280.411,268.856],[272.544,276.642],[272.544,419.386],[280.411,427.172],[332.339,427.172],[338.111,425.096],[340.208,427.172],[464.514,426.652],[517.49,333.736],[523.785,308.82],[521.16,276.637]],"c":true}},"o":{"k":100},"x":{"k":0},"nm":"蒙版 3"},{"inv":false,"mode":"s","pt":{"k":{"i":[[0,0],[2.097,-9.861],[18.887,0],[6.816,0],[0,0],[-1.052,0.52],[5.769,62.291],[0.525,0],[-3.147,0],[0,0],[-2.098,-4.669],[12.067,-18.166],[-1.052,-2.596],[-21.503,0],[0,0],[-2.101,-2.597],[2.618,-8.822]],"o":[[-2.098,7.267],[-7.343,32.183],[-30.419,0],[0,0],[1.047,0],[2.624,-1.036],[0,-2.597],[0.525,-0.52],[0,0],[2.098,0],[8.392,15.571],[-4.199,6.23],[4.198,7.267],[0,0],[2.098,0],[3.671,2.597],[0,0]],"v":[[508.045,304.672],[501.75,330.626],[463.984,411.599],[339.679,412.119],[339.679,287.025],[342.303,286.505],[406.292,193.589],[406.292,189.956],[411.537,189.435],[413.634,189.435],[420.977,196.702],[418.353,259.51],[416.781,273.524],[452.971,283.903],[498.081,283.903],[507.524,286.5],[508.049,304.668]],"c":true}},"o":{"k":100},"x":{"k":0},"nm":"蒙版 1"},{"inv":false,"mode":"s","pt":{"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[323.943,411.599],[287.753,411.599],[287.753,284.428],[323.943,284.428]],"c":true}},"o":{"k":100},"x":{"k":0},"nm":"蒙版 2"}],"sw":800,"sh":600,"sc":"#00b1ff","ip":0,"op":50,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":2,"ty":1,"nm":"品蓝色 纯色 2","ks":{"o":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":11,"s":[0],"e":[27]},{"t":16}]},"r":{"k":0},"p":{"k":[400,300,0]},"a":{"k":[400,300,0]},"s":{"k":[100,100,100]}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"f","pt":{"k":{"i":[[0,0],[3.675,0],[0,0],[2.623,1.039],[10.49,18.686],[8.393,0],[0,0],[5.767,-5.706],[-0.526,-6.747],[8.391,-4.158],[2.101,0],[0,0],[0,-4.152],[0,0],[-4.196,0],[0,0],[-1.574,1.04],[-1.051,-0.52],[-31.47,0],[-9.441,41.527],[-2.097,6.748],[5.768,7.267]],"o":[[-7.342,-8.823],[0,0],[-12.065,0],[14.688,-23.358],[-5.772,-9.866],[0,0],[-3.671,-0.52],[-3.672,3.633],[4.198,46.715],[-1.572,-1.556],[0,0],[-4.198,0],[0,0],[0,4.155],[0,0],[2.097,0],[0.525,1.036],[0,0],[31.47,0],[2.098,-9.345],[3.671,-14.014],[0,0]],"v":[[521.158,276.64],[498.604,267.817],[454.021,267.817],[433.565,265.741],[435.663,188.92],[414.159,173.864],[413.112,173.864],[395.279,178.534],[390.561,194.626],[337.06,270.932],[331.816,268.856],[280.411,268.856],[272.544,276.642],[272.544,419.386],[280.411,427.172],[332.339,427.172],[338.111,425.096],[340.208,427.172],[464.514,426.652],[517.49,333.736],[523.785,308.82],[521.16,276.637]],"c":true}},"o":{"k":100},"x":{"k":0},"nm":"蒙版 1"}],"sw":800,"sh":600,"sc":"#00b1ff","ip":0,"op":50,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":1,"nm":"灰色 纯色 1","ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[400,300,0]},"a":{"k":[400,300,0]},"s":{"k":[100,100,100]}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"k":{"i":[[0,0],[3.675,0],[0,0],[2.623,1.039],[10.49,18.686],[8.393,0],[0,0],[5.767,-5.706],[-0.526,-6.747],[8.391,-4.158],[2.101,0],[0,0],[0,-4.152],[0,0],[-4.196,0],[0,0],[-1.574,1.04],[-1.051,-0.52],[-31.47,0],[-9.441,41.527],[-2.097,6.748],[5.768,7.267]],"o":[[-7.342,-8.823],[0,0],[-12.065,0],[14.688,-23.358],[-5.772,-9.866],[0,0],[-3.671,-0.52],[-3.672,3.633],[4.198,46.715],[-1.572,-1.556],[0,0],[-4.198,0],[0,0],[0,4.155],[0,0],[2.097,0],[0.525,1.036],[0,0],[31.47,0],[2.098,-9.345],[3.671,-14.014],[0,0]],"v":[[521.158,276.64],[498.604,267.817],[454.021,267.817],[433.565,265.741],[435.663,188.92],[414.159,173.864],[413.112,173.864],[395.279,178.534],[390.561,194.626],[337.06,270.932],[331.816,268.856],[280.411,268.856],[272.544,276.642],[272.544,419.386],[280.411,427.172],[332.339,427.172],[338.111,425.096],[340.208,427.172],[464.514,426.652],[517.49,333.736],[523.785,308.82],[521.16,276.637]],"c":true}},"o":{"k":100},"x":{"k":0},"nm":"蒙版 3"},{"inv":false,"mode":"s","pt":{"k":{"i":[[0,0],[2.097,-9.861],[18.887,0],[6.816,0],[0,0],[-1.052,0.52],[5.769,62.291],[0.525,0],[-3.147,0],[0,0],[-2.098,-4.669],[12.067,-18.166],[-1.052,-2.596],[-21.503,0],[0,0],[-2.101,-2.597],[2.618,-8.822]],"o":[[-2.098,7.267],[-7.343,32.183],[-30.419,0],[0,0],[1.047,0],[2.624,-1.036],[0,-2.597],[0.525,-0.52],[0,0],[2.098,0],[8.392,15.571],[-4.199,6.23],[4.198,7.267],[0,0],[2.098,0],[3.671,2.597],[0,0]],"v":[[508.045,304.672],[501.75,330.626],[463.984,411.599],[339.679,412.119],[339.679,287.025],[342.303,286.505],[406.292,193.589],[406.292,189.956],[411.537,189.435],[413.634,189.435],[420.977,196.702],[418.353,259.51],[416.781,273.524],[452.971,283.903],[498.081,283.903],[507.524,286.5],[508.049,304.668]],"c":true}},"o":{"k":100},"x":{"k":0},"nm":"蒙版 1"},{"inv":false,"mode":"s","pt":{"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[323.943,411.599],[287.753,411.599],[287.753,284.428],[323.943,284.428]],"c":true}},"o":{"k":100},"x":{"k":0},"nm":"蒙版 2"}],"sw":800,"sh":600,"sc":"#8c8c8c","ip":0,"op":37,"st":0,"bm":0,"sr":1}],"v":"4.5.4","ddd":0,"ip":0,"op":50,"fr":25,"w":800,"h":600} -------------------------------------------------------------------------------- /samples/sample.tgs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laggykiller/rlottie-python/21a588d75da9fb5ae32ebe79abf981badb4e0553/samples/sample.tgs -------------------------------------------------------------------------------- /src/rlottie_python/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """rlottie-python""" 3 | 4 | __version__ = "1.3.7" 5 | 6 | from .rlottie_wrapper import LottieAnimation # type: ignore # noqa: F401 7 | from .rlottie_wrapper import LottieAnimationProperty # type: ignore # noqa: F401 8 | -------------------------------------------------------------------------------- /src/rlottie_python/_rlottiecommon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from ctypes import ( 3 | POINTER, 4 | Structure, 5 | c_char, 6 | c_char_p, 7 | c_float, 8 | c_int, 9 | c_size_t, 10 | c_ubyte, 11 | ) 12 | 13 | # References: rlottie/inc/rlottiecommon.h 14 | 15 | 16 | # LOTMarkerList -> LOTMarker 17 | class LOTMarker(Structure): 18 | _fields_ = [("name", c_char_p), ("startframe", c_size_t), ("endframe", c_size_t)] 19 | 20 | 21 | # LOTMarkerList 22 | class LOTMarkerList(Structure): 23 | _fields_ = [("ptr", POINTER(LOTMarker)), ("size", c_size_t)] 24 | 25 | 26 | # LOTLayerNode 27 | class LOTLayerNode(Structure): 28 | pass 29 | 30 | 31 | # mClipPath -> mMaskList -> LOTMask -> mPath 32 | class mPath(Structure): 33 | _fields_ = [ 34 | ("ptPtr", POINTER(c_float)), 35 | ("ptCount", c_size_t), 36 | ("elmPtr", c_char_p), 37 | ("elmCount", c_size_t), 38 | ] 39 | 40 | 41 | # mClipPath -> mMaskList -> LOTMask 42 | class LOTMask(Structure): 43 | _fields_ = [("mPath", mPath), ("mMode", c_int), ("mAlpha", c_ubyte)] 44 | 45 | 46 | # mClipPath -> mMaskList 47 | class mMaskList(Structure): 48 | _fields_ = [("ptr", POINTER(LOTMask)), ("size", c_size_t)] 49 | 50 | 51 | # LOTLayerNode -> mClipPath 52 | class mClipPath(Structure): 53 | _fields_ = [ 54 | ("ptPtr", POINTER(c_float)), 55 | ("ptCount", c_size_t), 56 | ("elmPtr", c_char), 57 | ("elmCount", c_size_t), 58 | ] 59 | 60 | 61 | # LOTLayerNode -> mLayerList 62 | class mLayerList(Structure): 63 | _fields_ = [("ptr", POINTER(POINTER(LOTLayerNode))), ("size", c_size_t)] 64 | 65 | 66 | # LOTLayerNode -> mNodeList -> LOTNode -> mColor 67 | class mColor(Structure): 68 | _fields_ = [("r", c_ubyte), ("g", c_ubyte), ("b", c_ubyte), ("a", c_ubyte)] 69 | 70 | 71 | # LOTLayerNode -> mNodeList -> LOTNode -> mStroke 72 | class mStroke(Structure): 73 | _fields_ = [ 74 | ("enable", c_ubyte), 75 | ("width", c_float), 76 | ("cap", c_int), 77 | ("join", c_int), 78 | ("miterLimit", c_float), 79 | ("dashArray", c_float), 80 | ("dashArraySize", c_int), 81 | ] 82 | 83 | 84 | # LOTLayerNode -> mNodeList -> LOTNode -> mGradient -> LOTGradientStop 85 | class LOTGradientStop(Structure): 86 | _fields_ = [ 87 | ("pos", c_float), 88 | ("r", c_ubyte), 89 | ("g", c_ubyte), 90 | ("b", c_ubyte), 91 | ("a", c_ubyte), 92 | ] 93 | 94 | 95 | # LOTLayerNode -> mNodeList -> LOTNode -> mGradient -> start,end,center,focal 96 | class coords(Structure): 97 | _fields_ = [("x", c_float), ("y", c_float)] 98 | 99 | 100 | # LOTLayerNode -> mNodeList -> LOTNode -> mGradient 101 | class mGradient(Structure): 102 | _fields_ = [ 103 | ("type", c_int), 104 | ("stropPtr", LOTGradientStop), 105 | ("stopCount", c_size_t), 106 | ("start", coords), 107 | ("end", coords), 108 | ("center", coords), 109 | ("focal", coords), 110 | ("cradius", c_float), 111 | ("fradius", c_float), 112 | ] 113 | 114 | 115 | # LOTLayerNode -> mNodeList -> LOTNode -> mImageInfo -> mMatrix 116 | class mMatrix(Structure): 117 | _fields_ = [ 118 | ("m11", c_float), 119 | ("m12", c_float), 120 | ("m13", c_float), 121 | ("m21", c_float), 122 | ("m22", c_float), 123 | ("m23", c_float), 124 | ("m31", c_float), 125 | ("m32", c_float), 126 | ("m33", c_float), 127 | ] 128 | 129 | 130 | # LOTLayerNode -> mNodeList -> LOTNode -> mImageInfo 131 | class mImageInfo(Structure): 132 | _fields_ = [ 133 | ("data", c_ubyte), 134 | ("width", c_size_t), 135 | ("height", c_size_t), 136 | ("mAlpha", c_ubyte), 137 | ("mMatrix", mMatrix), 138 | ] 139 | 140 | 141 | # LOTLayerNode -> mNodeList -> LOTNode 142 | class LOTNode(Structure): 143 | _fields_ = [ 144 | ("mPath", mPath), 145 | ("mColor", mColor), 146 | ("mStroke", mStroke), 147 | ("mGradient", mGradient), 148 | ("mImageInfo", mImageInfo), 149 | ("mFlag", c_int), 150 | ("mBrushType", c_int), 151 | ("mFillRule", c_int), 152 | ("keypath", c_char_p), 153 | ] 154 | 155 | 156 | # LOTLayerNode -> mNodeList 157 | class mNodeList(Structure): 158 | _fields_ = [("ptr", POINTER(POINTER(LOTNode))), ("size", c_size_t)] 159 | 160 | 161 | LOTLayerNode._fields_ = [ 162 | ("mMaskList", mMaskList), 163 | ("mClipPath", mClipPath), 164 | ("mLayerList", mLayerList), 165 | ("mNodeList", mNodeList), 166 | ("mMatte", c_int), 167 | ("mVisible", c_int), 168 | ("mAlpha", c_ubyte), 169 | ("keypath", c_char_p), 170 | ] 171 | -------------------------------------------------------------------------------- /src/rlottie_python/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laggykiller/rlottie-python/21a588d75da9fb5ae32ebe79abf981badb4e0553/src/rlottie_python/py.typed -------------------------------------------------------------------------------- /src/rlottie_python/rlottie_wrapper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import ctypes 3 | import gzip 4 | import os 5 | import sys 6 | import sysconfig 7 | from enum import IntEnum 8 | from types import TracebackType 9 | from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Type 10 | 11 | if TYPE_CHECKING: 12 | from PIL import Image 13 | 14 | from ._rlottiecommon import LOTLayerNode, LOTMarkerList 15 | 16 | # References: rlottie/inc/rlottie_capi.h 17 | 18 | 19 | def _load_lib_with_prefix_suffix( 20 | lib_prefix: str, lib_suffix: str 21 | ) -> Optional[ctypes.CDLL]: 22 | package_dir = os.path.dirname(__file__) 23 | rlottie_lib_name = lib_prefix + "rlottie" + lib_suffix 24 | rlottie_lib_path_local = os.path.join(package_dir, rlottie_lib_name) 25 | 26 | if os.path.isfile(rlottie_lib_path_local): 27 | rlottie_lib_path = rlottie_lib_path_local 28 | elif os.path.isfile(rlottie_lib_name): 29 | rlottie_lib_path = os.path.abspath(rlottie_lib_name) 30 | else: 31 | rlottie_lib_path = rlottie_lib_name 32 | 33 | try: 34 | return ctypes.cdll.LoadLibrary(rlottie_lib_path) 35 | except OSError: 36 | return None 37 | 38 | 39 | def _load_lib(rlottie_lib_path: Optional[str] = None) -> Optional[ctypes.CDLL]: 40 | if rlottie_lib_path: 41 | try: 42 | return ctypes.cdll.LoadLibrary(rlottie_lib_path) 43 | except OSError: 44 | return None 45 | 46 | if sys.platform.startswith(("win32", "cygwin", "msys", "os2")): 47 | lib = _load_lib_with_prefix_suffix("", ".dll") 48 | elif sys.platform.startswith("darwin"): 49 | lib = _load_lib_with_prefix_suffix("lib", ".dylib") 50 | else: 51 | lib = _load_lib_with_prefix_suffix("lib", ".so") 52 | 53 | if lib: 54 | return lib 55 | 56 | lib_suffixes: List[str] = [] 57 | shlib_suffix = sysconfig.get_config_var("SHLIB_SUFFIX") 58 | if isinstance(shlib_suffix, str): 59 | lib_suffixes.append(shlib_suffix) 60 | if sys.platform.startswith(("win32", "cygwin", "msys", "os2")): 61 | lib_prefixes = ("", "lib") 62 | elif sys.platform.startswith("darwin"): 63 | lib_prefixes = ("lib", "") 64 | else: 65 | lib_prefixes = ("lib", "") 66 | lib_suffixes.extend([".so", ".dll", ".dylib"]) 67 | 68 | for lib_prefix in lib_prefixes: 69 | for lib_suffix in set(lib_suffixes): 70 | lib = _load_lib_with_prefix_suffix(lib_prefix, lib_suffix) 71 | if lib: 72 | return lib 73 | 74 | return None 75 | 76 | 77 | RLOTTIE_LIB = _load_lib() 78 | 79 | 80 | class _LottieAnimationPointer(ctypes.c_void_p): 81 | pass 82 | 83 | 84 | class LottieAnimationProperty(IntEnum): 85 | LOTTIE_ANIMATION_PROPERTY_FILLCOLOR = 0 86 | LOTTIE_ANIMATION_PROPERTY_FILLOPACITY = 1 87 | LOTTIE_ANIMATION_PROPERTY_STROKECOLOR = 2 88 | LOTTIE_ANIMATION_PROPERTY_STROKEOPACITY = 3 89 | LOTTIE_ANIMATION_PROPERTY_STROKEWIDTH = 4 90 | LOTTIE_ANIMATION_PROPERTY_TR_ANCHOR = 5 91 | LOTTIE_ANIMATION_PROPERTY_TR_POSITION = 6 92 | LOTTIE_ANIMATION_PROPERTY_TR_SCALE = 7 93 | LOTTIE_ANIMATION_PROPERTY_TR_ROTATION = 8 94 | LOTTIE_ANIMATION_PROPERTY_TR_OPACITY = 9 95 | 96 | @classmethod 97 | def from_param(cls, obj: int) -> int: 98 | return int(obj) 99 | 100 | 101 | class LottieAnimation: 102 | def __init__( 103 | self, 104 | path: str = "", 105 | data: str = "", 106 | key_size: Optional[int] = None, 107 | resource_path: Optional[str] = None, 108 | rlottie_lib_path: Optional[str] = None, 109 | ) -> None: 110 | self.animation_p = None 111 | self.data_c: Optional[ctypes.Array[ctypes.c_char]] = None 112 | self.key_c: Optional[ctypes.Array[ctypes.c_char]] = None 113 | self.resource_path_abs_c: Optional[ctypes.Array[ctypes.c_char]] = None 114 | self.async_buffer_c: Optional[ctypes.Array[ctypes.c_char]] = None 115 | 116 | self._load_lib(rlottie_lib_path) 117 | self.lottie_init() 118 | self.lottie_configure_model_cache_size(0) 119 | 120 | if path != "": 121 | self.lottie_animation_from_file(path=path) 122 | else: 123 | self.lottie_animation_from_data( 124 | data=data, key_size=key_size, resource_path=resource_path 125 | ) 126 | 127 | def _load_lib(self, rlottie_lib_path: Optional[str] = None) -> None: 128 | if rlottie_lib_path is None: 129 | if RLOTTIE_LIB is None: 130 | raise OSError("Could not load rlottie library") 131 | else: 132 | self.rlottie_lib = RLOTTIE_LIB 133 | return 134 | 135 | rlottie_lib = _load_lib(rlottie_lib_path) 136 | if rlottie_lib is None: 137 | raise OSError(f"Could not load rlottie library from {rlottie_lib_path}") 138 | else: 139 | self.rlottie_lib = rlottie_lib 140 | 141 | def __del__(self) -> None: 142 | if self.rlottie_lib: 143 | self.lottie_animation_destroy() 144 | 145 | def __enter__(self) -> "LottieAnimation": 146 | return self 147 | 148 | def __exit__( 149 | self, 150 | exc_type: Optional[Type[BaseException]], 151 | exc_val: Optional[BaseException], 152 | exc_tb: Optional[TracebackType], 153 | ) -> None: 154 | if self.rlottie_lib: 155 | self.lottie_animation_destroy() 156 | 157 | @classmethod 158 | def from_file( 159 | cls, path: str, rlottie_lib_path: Optional[str] = None 160 | ) -> "LottieAnimation": 161 | """ 162 | Constructs LottieAnimation object from lottie file path. 163 | 164 | :param str path: lottie resource file path 165 | :param Optional[str] rlottie_lib_path: Optionally specific where the rlottie 166 | library is located 167 | 168 | :return: LottieAnimation object 169 | :rtype: LottieAnimation 170 | """ 171 | return cls(path=path, rlottie_lib_path=rlottie_lib_path) 172 | 173 | @classmethod 174 | def from_data( 175 | cls, 176 | data: str, 177 | key_size: Optional[int] = None, 178 | resource_path: Optional[str] = None, 179 | rlottie_lib_path: Optional[str] = None, 180 | ) -> "LottieAnimation": 181 | """ 182 | Constructs LottieAnimation object from JSON string data. 183 | 184 | :param str data: the JSON string data. 185 | :param Optional[int] key_size: the string that will be used to 186 | cache the JSON string data. 187 | :param Optional[str] resource_path: the path that will be used to 188 | load external resource needed by the JSON data. 189 | :param Optional[str] rlottie_lib_path: Optionally specific where the rlottie 190 | library is located 191 | 192 | :return: LottieAnimation object 193 | :rtype: LottieAnimation 194 | """ 195 | return cls( 196 | data=data, 197 | key_size=key_size, 198 | resource_path=resource_path, 199 | rlottie_lib_path=rlottie_lib_path, 200 | ) 201 | 202 | @classmethod 203 | def from_tgs( 204 | cls, path: str, rlottie_lib_path: Optional[str] = None 205 | ) -> "LottieAnimation": 206 | """ 207 | Constructs LottieAnimation object from tgs file path. 208 | 209 | :param str path: tgs resource file path 210 | :param Optional[str] rlottie_lib_path: Optionally specific where the rlottie 211 | library is located 212 | 213 | :return: LottieAnimation object 214 | :rtype: LottieAnimation 215 | """ 216 | with gzip.open(path) as f: 217 | data = f.read().decode(encoding="utf-8") 218 | return cls(data=data, rlottie_lib_path=rlottie_lib_path) 219 | 220 | def lottie_init(self) -> None: 221 | """ 222 | Runs lottie initialization code when rlottie library is loaded 223 | dynamically. 224 | 225 | This api should be called before any other api when rlottie library 226 | is loaded using dlopen() or equivalent. 227 | """ 228 | self.rlottie_lib.lottie_init.argtypes = [] 229 | self.rlottie_lib.lottie_init.restype = ctypes.c_void_p 230 | self.rlottie_lib.lottie_init() 231 | 232 | def lottie_shutdown(self) -> None: 233 | """ 234 | Runs lottie teardown code when rlottie library is loaded 235 | dynamically. 236 | 237 | This api should be called before unloading the rlottie library for 238 | proper cleanup of the resource without doing so will result in undefined 239 | behaviour. 240 | """ 241 | self.rlottie_lib.lottie_shutdown.argtypes = [] 242 | self.rlottie_lib.lottie_shutdown.restype = ctypes.c_void_p 243 | self.rlottie_lib.lottie_shutdown() 244 | 245 | def lottie_animation_from_file(self, path: str) -> None: 246 | """ 247 | Constructs an animation object from JSON file path. 248 | 249 | .. note:: 250 | You should use from_file(path=path) instead 251 | 252 | :param str path: lottie resource file path 253 | """ 254 | self.rlottie_lib.lottie_animation_from_file.argtypes = [ctypes.c_char_p] 255 | self.rlottie_lib.lottie_animation_from_file.restype = _LottieAnimationPointer 256 | 257 | if not os.path.isabs(path): 258 | path = os.path.abspath(path) 259 | if not os.path.isfile(path): 260 | raise OSError(f"Cannot find file {path}") 261 | 262 | path_p = ctypes.c_char_p(path.encode()) 263 | 264 | self.animation_p = self.rlottie_lib.lottie_animation_from_file(path_p) 265 | 266 | del path_p 267 | 268 | def lottie_animation_from_data( 269 | self, 270 | data: str, 271 | key_size: Optional[int] = None, 272 | resource_path: Optional[str] = None, 273 | ) -> None: 274 | """ 275 | Constructs an animation object from JSON string data. 276 | 277 | .. note:: 278 | You should use from_data(data=data) instead 279 | 280 | :param str data: the JSON string data. 281 | :param Optional[int] key_size: the size of string that will be used to 282 | cache the JSON string data. 283 | :param Optional[str] resource_path: the path that will be used to 284 | load external resource needed by the JSON data. 285 | """ 286 | self.data_c = ctypes.create_string_buffer(data.encode()) 287 | data_size = ctypes.sizeof(self.data_c) 288 | 289 | if key_size is None: 290 | key_size = data_size 291 | self.key_c = ctypes.create_string_buffer(key_size) 292 | 293 | resource_path_abs = "" 294 | if resource_path is not None: 295 | resource_path_abs = os.path.abspath(resource_path) 296 | self.resource_path_abs_c = ctypes.create_string_buffer( 297 | resource_path_abs.encode() 298 | ) 299 | resource_path_abs_size = ctypes.sizeof(self.resource_path_abs_c) 300 | 301 | self.rlottie_lib.lottie_animation_from_data.argtypes = [ 302 | ctypes.POINTER(ctypes.c_char * data_size), 303 | ctypes.POINTER(ctypes.c_char * key_size), 304 | ctypes.POINTER(ctypes.c_char * resource_path_abs_size), 305 | ] 306 | self.rlottie_lib.lottie_animation_from_data.restype = _LottieAnimationPointer 307 | 308 | self.animation_p = self.rlottie_lib.lottie_animation_from_data( 309 | ctypes.byref(self.data_c), 310 | ctypes.byref(self.key_c), 311 | ctypes.byref(self.resource_path_abs_c), 312 | ) 313 | 314 | def lottie_animation_destroy(self) -> None: 315 | """ 316 | Free given Animation object resource. 317 | 318 | Call this before loading new lottie animation. 319 | """ 320 | if not self.animation_p: 321 | return 322 | 323 | self.rlottie_lib.lottie_animation_destroy.argtypes = [_LottieAnimationPointer] 324 | self.rlottie_lib.lottie_animation_destroy.restype = ctypes.c_void_p 325 | self.rlottie_lib.lottie_animation_destroy(self.animation_p) 326 | 327 | del self.animation_p 328 | self.animation_p = None 329 | 330 | if self.data_c: 331 | del self.data_c 332 | self.data_c = None 333 | if self.key_c: 334 | del self.key_c 335 | self.key_c = None 336 | if self.resource_path_abs_c: 337 | del self.resource_path_abs_c 338 | self.resource_path_abs_c = None 339 | 340 | def lottie_animation_get_size(self) -> Tuple[int, int]: 341 | """ 342 | Returns default viewport size of the Lottie resource. 343 | 344 | :return: width, height 345 | :rtype: Tuple[int, int] 346 | """ 347 | self.rlottie_lib.lottie_animation_get_size.argtypes = [ 348 | _LottieAnimationPointer, 349 | ctypes.POINTER(ctypes.c_size_t), 350 | ctypes.POINTER(ctypes.c_size_t), 351 | ] 352 | self.rlottie_lib.lottie_animation_get_size.restype = ctypes.c_void_p 353 | 354 | width_c = ctypes.c_size_t() 355 | height_c = ctypes.c_size_t() 356 | self.rlottie_lib.lottie_animation_get_size( 357 | self.animation_p, ctypes.byref(width_c), ctypes.byref(height_c) 358 | ) 359 | 360 | width = width_c.value 361 | height = height_c.value 362 | 363 | del width_c, height_c 364 | 365 | return width, height 366 | 367 | def lottie_animation_get_duration(self) -> int: 368 | """ 369 | Returns total animation duration of Lottie resource in second. 370 | 371 | :return: duration 372 | :rtype: int 373 | """ 374 | self.rlottie_lib.lottie_animation_get_duration.argtypes = [ 375 | _LottieAnimationPointer 376 | ] 377 | self.rlottie_lib.lottie_animation_get_duration.restype = ctypes.c_double 378 | 379 | duration = self.rlottie_lib.lottie_animation_get_duration(self.animation_p) 380 | return duration 381 | 382 | def lottie_animation_get_totalframe(self) -> int: 383 | """ 384 | Returns total number of frames present in the Lottie resource. 385 | 386 | :return: totalframe 387 | :rtype: int 388 | """ 389 | totalframe = self.rlottie_lib.lottie_animation_get_totalframe(self.animation_p) 390 | 391 | return totalframe 392 | 393 | def lottie_animation_get_framerate(self) -> int: 394 | """ 395 | Returns default framerate of the Lottie resource. 396 | 397 | :return: framerate 398 | :rtype: int 399 | """ 400 | self.rlottie_lib.lottie_animation_get_framerate.argtypes = [ 401 | _LottieAnimationPointer 402 | ] 403 | self.rlottie_lib.lottie_animation_get_framerate.restype = ctypes.c_double 404 | 405 | framerate = self.rlottie_lib.lottie_animation_get_framerate(self.animation_p) 406 | 407 | return framerate 408 | 409 | def lottie_animation_render_tree( 410 | self, 411 | frame_num: int = 0, 412 | width: Optional[int] = None, 413 | height: Optional[int] = None, 414 | ) -> LOTLayerNode: 415 | """ 416 | Get the render tree which contains the snapshot of the animation object 417 | at frame = @c frame_num, the content of the animation in that frame number. 418 | 419 | Example for getting content of render_tree: render_tree.mMaskList.size 420 | 421 | :param int frame_num: Content corresponds to the frame_num needs 422 | to be drawn. Defaults to 0. 423 | :param Optional[int] width: Requested snapshot viewport width. 424 | :param Optional[int] height: Requested snapshot viewport height. 425 | 426 | :return: animation snapshot tree. 427 | :rtype: rlottie_python.rlottiecommon.LOTLayerNode 428 | """ 429 | self.rlottie_lib.lottie_animation_render_tree.argtypes = [ 430 | _LottieAnimationPointer, 431 | ctypes.c_size_t, 432 | ctypes.c_size_t, 433 | ctypes.c_size_t, 434 | ] 435 | self.rlottie_lib.lottie_animation_render_tree.restype = ctypes.POINTER( 436 | LOTLayerNode 437 | ) 438 | 439 | if width is None or height is None: 440 | width, height = self.lottie_animation_get_size() 441 | 442 | render_tree_p = self.rlottie_lib.lottie_animation_render_tree( 443 | self.animation_p, 444 | ctypes.c_size_t(frame_num), 445 | ctypes.c_size_t(width), 446 | ctypes.c_size_t(height), 447 | ) 448 | 449 | render_tree = render_tree_p.contents 450 | 451 | del render_tree_p 452 | 453 | return render_tree 454 | 455 | def lottie_animation_get_frame_at_pos(self, pos: float) -> int: 456 | """ 457 | Maps position to frame number and returns it. 458 | 459 | :param float pos: position in the range [ 0.0 .. 1.0 ]. 460 | 461 | :return: Mapped frame number in the range [ start_frame .. end_frame ]. 462 | 0 if the Lottie resource has no animation. 463 | :rtype: int 464 | """ 465 | self.rlottie_lib.lottie_animation_get_frame_at_pos.argtypes = [ 466 | _LottieAnimationPointer, 467 | ctypes.c_float, 468 | ] 469 | self.rlottie_lib.lottie_animation_get_frame_at_pos.restype = ctypes.c_size_t 470 | 471 | mapped_frame = self.rlottie_lib.lottie_animation_get_frame_at_pos( 472 | self.animation_p, ctypes.c_float(pos) 473 | ) 474 | 475 | return mapped_frame 476 | 477 | def lottie_animation_render( 478 | self, 479 | frame_num: int = 0, 480 | buffer_size: Optional[int] = None, 481 | width: Optional[int] = None, 482 | height: Optional[int] = None, 483 | bytes_per_line: Optional[int] = None, 484 | ) -> bytes: 485 | """ 486 | Request to render the content of the frame frame_num to buffer. 487 | 488 | :param int frame_num: the frame number needs to be rendered. 489 | Defaults to 0. 490 | :param Optional[int] buffer_size: size of surface buffer use for rendering 491 | :param Optional[int] width: width of the surface 492 | :param Optional[int] height: height of the surface 493 | :param Optional[int] bytes_per_line: stride of the surface in bytes. 494 | 495 | :return: rendered surface buffer 496 | :rtype: bytes 497 | """ 498 | if width is None or height is None: 499 | width, height = self.lottie_animation_get_size() 500 | 501 | if bytes_per_line is None: 502 | bytes_per_line = width * 4 503 | 504 | if buffer_size is None: 505 | buffer_size = width * height * 4 506 | 507 | buffer_c = ctypes.create_string_buffer(buffer_size) 508 | 509 | self.rlottie_lib.lottie_animation_render.argtypes = [ 510 | _LottieAnimationPointer, 511 | ctypes.c_size_t, 512 | ctypes.POINTER(ctypes.c_char * buffer_size), 513 | ctypes.c_size_t, 514 | ctypes.c_size_t, 515 | ctypes.c_size_t, 516 | ] 517 | self.rlottie_lib.lottie_animation_render.restype = ctypes.c_void_p 518 | 519 | self.rlottie_lib.lottie_animation_render( 520 | self.animation_p, 521 | ctypes.c_size_t(frame_num), 522 | ctypes.byref(buffer_c), 523 | ctypes.c_size_t(width), 524 | ctypes.c_size_t(height), 525 | ctypes.c_size_t(bytes_per_line), 526 | ) 527 | 528 | buffer = buffer_c.raw 529 | 530 | del buffer_c 531 | 532 | return buffer 533 | 534 | def lottie_animation_render_async( 535 | self, 536 | frame_num: int = 0, 537 | buffer_size: Optional[int] = None, 538 | width: Optional[int] = None, 539 | height: Optional[int] = None, 540 | bytes_per_line: Optional[int] = None, 541 | ) -> None: 542 | """ 543 | Request to render the content of the frame frame_num to buffer asynchronously. 544 | 545 | User must call lottie_animation_render_flush() to make sure render is finished. 546 | 547 | :param int frame_num: the frame number needs to be rendered. 548 | Defaults to 0. 549 | :param Optional[int] buffer_size: size of surface buffer use for rendering 550 | :param Optional[int] width: width of the surface 551 | :param Optional[int] height: height of the surface 552 | :param Optional[int] bytes_per_line: stride of the surface in bytes. 553 | """ 554 | if width is None or height is None: 555 | width, height = self.lottie_animation_get_size() 556 | 557 | if bytes_per_line is None: 558 | bytes_per_line = width * 4 559 | 560 | if buffer_size is None: 561 | buffer_size = width * height * 4 562 | 563 | self.async_buffer_c = ctypes.create_string_buffer(buffer_size) 564 | 565 | self.rlottie_lib.lottie_animation_render_async.argtypes = [ 566 | _LottieAnimationPointer, 567 | ctypes.c_size_t, 568 | ctypes.POINTER(ctypes.c_char * buffer_size), 569 | ctypes.c_size_t, 570 | ctypes.c_size_t, 571 | ctypes.c_size_t, 572 | ] 573 | self.rlottie_lib.lottie_animation_render_async.restype = ctypes.c_void_p 574 | 575 | self.rlottie_lib.lottie_animation_render_async( 576 | self.animation_p, 577 | ctypes.c_size_t(frame_num), 578 | ctypes.byref(self.async_buffer_c), 579 | ctypes.c_size_t(width), 580 | ctypes.c_size_t(height), 581 | ctypes.c_size_t(bytes_per_line), 582 | ) 583 | 584 | def lottie_animation_render_flush(self) -> bytes: 585 | """ 586 | Request to finish the current async renderer job for this animation object. 587 | 588 | If render is finished then this call returns immidiately. 589 | 590 | If not, it waits till render job finish and then return. 591 | 592 | User must call lottie_animation_render_async() 593 | and lottie_animation_render_flush() 594 | in pair to get the benefit of async rendering. 595 | 596 | :return: the pixel buffer it finised rendering 597 | :rtype: bytes 598 | """ 599 | if not self.async_buffer_c: 600 | raise AttributeError( 601 | "Nothing was rendered using lottie_animation_render_async()" 602 | ) 603 | 604 | self.rlottie_lib.lottie_animation_get_duration.argtypes = [ 605 | _LottieAnimationPointer 606 | ] 607 | self.rlottie_lib.lottie_animation_get_duration.restype = ctypes.c_uint32 608 | 609 | self.rlottie_lib.lottie_animation_render_flush(self.animation_p) 610 | 611 | return bytes(self.async_buffer_c) 612 | 613 | def lottie_animation_property_override( 614 | self, _type: LottieAnimationProperty, keypath: str, *args: ctypes.c_double 615 | ) -> None: 616 | """ 617 | Request to change the properties of this animation object. 618 | 619 | Keypath should conatin object names separated by (.) 620 | and can handle globe(**) or wildchar(*) 621 | 622 | .. list-table:: Possible values of _types and args 623 | :header-rows: 1 624 | 625 | * - _type LottieAnimationProperty 626 | - No. of args 627 | - args value 628 | - Description 629 | * - LOTTIE_ANIMATION_PROPERTY_FILLCOLOR 630 | - 3 args 631 | - [0 ... 1] 632 | - Color property of Fill object: 633 | * - LOTTIE_ANIMATION_PROPERTY_FILLOPACITY 634 | - 1 args 635 | - [0 ... 100] 636 | - Opacity property of Fill object 637 | * - LOTTIE_ANIMATION_PROPERTY_STROKECOLOR 638 | - 3 args 639 | - [0 ... 1] 640 | - Color property of Stroke object 641 | * - LOTTIE_ANIMATION_PROPERTY_STROKEOPACITY 642 | - 1 args 643 | - [0 ... 100] 644 | - Opacity property of Stroke object 645 | * - LOTTIE_ANIMATION_PROPERTY_STROKEWIDTH 646 | - 1 args 647 | - [0 ... +inf] 648 | - Stroke width property of Stroke object 649 | * - LOTTIE_ANIMATION_PROPERTY_TR_ANCHOR 650 | - 0 args 651 | - Any 652 | - Transform Anchor property of Layer and Group object 653 | * - LOTTIE_ANIMATION_PROPERTY_TR_POSITION 654 | - 2 args 655 | - Any 656 | - Transform Position property of Layer and Group object 657 | * - LOTTIE_ANIMATION_PROPERTY_TR_SCALE 658 | - 2 args 659 | - Any 660 | - Transform Scale property of Layer and Group object 661 | * - LOTTIE_ANIMATION_PROPERTY_TR_ROTATION 662 | - 1 args 663 | - [0 ... 360] 664 | - Transform Scale property of Layer and Group object 665 | * - LOTTIE_ANIMATION_PROPERTY_TR_OPACITY 666 | - 0 args 667 | - Any 668 | - Transform Opacity property of Layer and Group object 669 | 670 | Example: 671 | To change fillcolor property of fill1 object in the 672 | layer1->group1->fill1 hirarchy to RED color: 673 | 674 | .. code-block:: python 675 | 676 | lottie_animation_property_override( 677 | LottieAnimationProperty.LOTTIE_ANIMATION_PROPERTY_FILLCOLOR, 678 | "layer1.group1.fill1", 679 | ctypes.c_double(1.0), 680 | ctypes.c_double(0.0), 681 | ctypes.c_double(0.0) 682 | ) 683 | 684 | If all the color property inside group1 needs to be changed to GREEN color: 685 | 686 | .. code-block:: python 687 | 688 | lottie_animation_property_override( 689 | LottieAnimationProperty.LOTTIE_ANIMATION_PROPERTY_FILLCOLOR, 690 | "**.group1.**", 691 | ctypes.c_double(0.0), 692 | ctypes.c_double(1.0), 693 | ctypes.c_double(0.0) 694 | ) 695 | 696 | :param LottieAnimationProperty _type: Property type. 697 | :param str keypath: Specific content of target. 698 | :param ctypes.c_double *args: Property values. 699 | """ 700 | argtypes: List[Any] = [ 701 | _LottieAnimationPointer, 702 | LottieAnimationProperty, 703 | ctypes.c_wchar_p, 704 | ] 705 | for _ in args: 706 | argtypes.append(ctypes.c_double) 707 | 708 | self.rlottie_lib.lottie_animation_property_override.argtypes = argtypes 709 | self.rlottie_lib.lottie_animation_property_override.restype = ctypes.c_void_p 710 | 711 | self.rlottie_lib.lottie_animation_property_override( 712 | self.animation_p, 713 | _type, 714 | ctypes.c_wchar_p(keypath), 715 | *args, 716 | ) 717 | 718 | def lottie_animation_get_markerlist(self) -> Optional[LOTMarkerList]: 719 | """ 720 | Returns list of markers in the Lottie resource 721 | 722 | LOTMarkerList has a LOTMarker list and size of list 723 | 724 | LOTMarker has the marker's name, start frame, and end frame. 725 | 726 | Example for getting content of markerlist: markerlist.ptr.name 727 | 728 | :return: The list of marker. If there is no marker, return None 729 | :rtype: Optional[LOTMarkerList] 730 | """ 731 | self.rlottie_lib.lottie_animation_get_markerlist.argtypes = [ 732 | _LottieAnimationPointer 733 | ] 734 | self.rlottie_lib.lottie_animation_get_markerlist.restype = ctypes.POINTER( 735 | LOTMarkerList 736 | ) 737 | 738 | markerlist = self.rlottie_lib.lottie_animation_get_markerlist(self.animation_p) 739 | 740 | try: 741 | return markerlist.contents 742 | except ValueError: # NULL pointer access 743 | return None 744 | 745 | def lottie_configure_model_cache_size(self, cache_size: int) -> None: 746 | """ 747 | Configures rlottie model cache policy. 748 | 749 | Provides Library level control to configure model cache policy. 750 | 751 | Setting it to 0 will disable 752 | the cache as well as flush all the previously cached content. 753 | 754 | To disable Caching, configure with 0 size. 755 | 756 | To flush the current Cache content, configure it with 0 and 757 | then reconfigure with the new size. 758 | 759 | :param int cache_size: Maximum Model Cache size. 760 | """ 761 | self.rlottie_lib.lottie_configure_model_cache_size.argtypes = [ctypes.c_size_t] 762 | self.rlottie_lib.lottie_configure_model_cache_size.restype = ctypes.c_void_p 763 | self.rlottie_lib.lottie_configure_model_cache_size(ctypes.c_size_t(cache_size)) 764 | 765 | def render_pillow_frame( 766 | self, 767 | frame_num: int = 0, 768 | buffer_size: Optional[int] = None, 769 | width: Optional[int] = None, 770 | height: Optional[int] = None, 771 | bytes_per_line: Optional[int] = None, 772 | ) -> "Image.Image": 773 | """ 774 | Create Pillow Image at frame_num 775 | 776 | :param int frame_num: the frame number needs to be rendered. 777 | Defaults to 0. 778 | :param Optional[int] buffer_size: size of surface buffer use for rendering 779 | :param Optional[int] width: width of the surface 780 | :param Optional[int] height: height of the surface 781 | :param Optional[int] bytes_per_line: stride of the surface in bytes. 782 | 783 | :return: rendered Pillow Image 784 | :rtype: PIL.Image.Image 785 | """ 786 | from PIL import Image 787 | 788 | if width is None or height is None: 789 | width, height = self.lottie_animation_get_size() 790 | 791 | buffer = self.lottie_animation_render( 792 | frame_num=frame_num, 793 | buffer_size=buffer_size, 794 | width=width, 795 | height=height, 796 | bytes_per_line=bytes_per_line, 797 | ) 798 | 799 | im = Image.frombuffer("RGBA", (width, height), buffer, "raw", "BGRA") # type: ignore 800 | 801 | return im 802 | 803 | def save_frame( 804 | self, 805 | save_path: str, 806 | frame_num: int = 0, 807 | buffer_size: Optional[int] = None, 808 | width: Optional[int] = None, 809 | height: Optional[int] = None, 810 | bytes_per_line: Optional[int] = None, 811 | *args: Any, 812 | **kwargs: Any, 813 | ) -> None: 814 | """ 815 | Save Image at frame_num to save_path 816 | 817 | :param str save_path: path to save the Pillow Image 818 | :param int frame_num: the frame number needs to be rendered. 819 | Defaults to 0. 820 | :param Optional[int] buffer_size: size of surface buffer use for rendering 821 | :param Optional[int] width: width of the surface 822 | :param Optional[int] height: height of the surface 823 | :param Optional[int] bytes_per_line: stride of the surface in bytes. 824 | :param Any *args: additional arguments passing to im.save() 825 | :param Any **kwargs: additional arguments passing to im.save() 826 | """ 827 | im = self.render_pillow_frame( 828 | frame_num=frame_num, 829 | buffer_size=buffer_size, 830 | width=width, 831 | height=height, 832 | bytes_per_line=bytes_per_line, 833 | ) 834 | im.save(save_path, *args, **kwargs) 835 | 836 | def save_animation( 837 | self, 838 | save_path: str, 839 | fps: Optional[int] = None, 840 | frame_num_start: Optional[int] = None, 841 | frame_num_end: Optional[int] = None, 842 | buffer_size: Optional[int] = None, 843 | width: Optional[int] = None, 844 | height: Optional[int] = None, 845 | bytes_per_line: Optional[int] = None, 846 | *args: Any, 847 | **kwargs: Any, 848 | ) -> None: 849 | """ 850 | Save Image from frame_num_start to frame_num_end and save it to save_path. 851 | 852 | It is possible to save animation as apng, gif or webp. 853 | 854 | For .gif, maximum framerate is capped at 50. 855 | 856 | Users may override this by specifying fps, at risk of breaking their gif. 857 | 858 | :param str save_path: Path to save the Pillow Image 859 | :param Optional[int] fps: Set fps of output image. 860 | Will skip frames if lower than original. 861 | :param Optional[int] frame_num_start: the starting frame number 862 | needs to be rendered. 863 | :param Optional[int] frame_num_end: the ending frame number 864 | needs to be rendered. 865 | :param Optional[int] buffer_size: size of surface buffer use for rendering 866 | :param Optional[int] width: width of the surface 867 | :param Optional[int] height: height of the surface 868 | :param Optional[int] bytes_per_line: stride of the surface in bytes. 869 | :param Any *args: additional arguments passing to im.save() 870 | :param Any **kwargs: additional arguments passing to im.save() 871 | """ 872 | fps_orig = self.lottie_animation_get_framerate() 873 | duration = self.lottie_animation_get_duration() 874 | 875 | export_ext = os.path.splitext(save_path)[-1].lower() 876 | 877 | if not fps: 878 | fps = fps_orig 879 | 880 | # For .gif, maximum framerate is capped at 50 881 | # Users may override this by specifying fps, at risk of breaking their gif 882 | # Reference: https://wunkolo.github.io/post/2020/02/buttery-smooth-10fps/ 883 | if export_ext == ".gif" and fps_orig > 50: 884 | fps = 50 885 | 886 | if export_ext == ".gif" and kwargs.get("disposal") is None: 887 | kwargs["disposal"] = 2 888 | 889 | if kwargs.get("loop") is None: 890 | kwargs["loop"] = 0 891 | 892 | frames = int(duration * fps) 893 | frame_duration = 1000 / fps 894 | 895 | if frame_num_start is None: 896 | frame_num_start = 0 897 | if frame_num_end is None: 898 | frame_num_end = frames 899 | 900 | im_list: List[Image.Image] = [] 901 | for frame in range(frame_num_start, frame_num_end): 902 | pos = frame / frame_num_end 903 | frame_num = self.lottie_animation_get_frame_at_pos(pos) 904 | im_list.append( 905 | self.render_pillow_frame( 906 | frame_num=frame_num, 907 | buffer_size=buffer_size, 908 | width=width, 909 | height=height, 910 | bytes_per_line=bytes_per_line, 911 | ).copy() 912 | ) 913 | 914 | im_list[0].save( 915 | save_path, 916 | save_all=True, 917 | append_images=im_list[1:], 918 | duration=int(frame_duration), 919 | *args, 920 | **kwargs, 921 | ) 922 | -------------------------------------------------------------------------------- /tests/test_all.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import ctypes 3 | import os 4 | import platform 5 | from importlib.util import find_spec 6 | 7 | import pytest 8 | from _pytest._py.path import LocalPath 9 | 10 | from rlottie_python import LottieAnimation, LottieAnimationProperty 11 | from rlottie_python._rlottiecommon import LOTLayerNode 12 | 13 | PILLOW_LOADED = True if find_spec("PIL") else False 14 | 15 | file_dir = os.path.split(__file__)[0] 16 | json_file = os.path.join(file_dir, "../samples/sample.json") 17 | tgs_file = os.path.join(file_dir, "../samples/sample.tgs") 18 | 19 | 20 | def test_from_file(): 21 | anim = LottieAnimation.from_file(json_file) 22 | assert isinstance(anim, LottieAnimation) 23 | 24 | 25 | def test_from_file_with(): 26 | with LottieAnimation.from_file(json_file) as anim: 27 | assert isinstance(anim, LottieAnimation) 28 | 29 | 30 | def test_from_data(): 31 | with open(json_file, encoding="utf-8") as f: 32 | data = f.read() 33 | 34 | anim = LottieAnimation.from_data(data) 35 | assert isinstance(anim, LottieAnimation) 36 | 37 | 38 | def test_from_data_with(): 39 | with open(json_file, encoding="utf-8") as f: 40 | data = f.read() 41 | 42 | with LottieAnimation.from_data(data) as anim: 43 | assert isinstance(anim, LottieAnimation) 44 | 45 | 46 | def test_from_tgs(): 47 | anim = LottieAnimation.from_tgs(tgs_file) 48 | assert isinstance(anim, LottieAnimation) 49 | 50 | 51 | def test_from_tgs_with(): 52 | with LottieAnimation.from_tgs(tgs_file) as anim: 53 | assert isinstance(anim, LottieAnimation) 54 | 55 | 56 | def test_lottie_animation_get_size(): 57 | with LottieAnimation.from_file(json_file) as anim: 58 | size = anim.lottie_animation_get_size() 59 | 60 | assert size == (800, 600) 61 | 62 | 63 | def test_lottie_animation_get_duration(): 64 | with LottieAnimation.from_file(json_file) as anim: 65 | duration = anim.lottie_animation_get_duration() 66 | 67 | assert duration == 2.0 68 | 69 | 70 | def test_lottie_animation_get_totalframe(): 71 | with LottieAnimation.from_file(json_file) as anim: 72 | totalframe = anim.lottie_animation_get_totalframe() 73 | 74 | assert totalframe == 51 75 | 76 | 77 | def test_lottie_animation_get_framerate(): 78 | with LottieAnimation.from_file(json_file) as anim: 79 | framerate = anim.lottie_animation_get_framerate() 80 | 81 | assert framerate == 25.0 82 | 83 | 84 | def test_lottie_animation_render_tree(): 85 | with LottieAnimation.from_file(json_file) as anim: 86 | render_tree = anim.lottie_animation_render_tree() 87 | 88 | assert isinstance(render_tree, LOTLayerNode) 89 | 90 | 91 | def test_lottie_animation_get_frame_at_pos(): 92 | with LottieAnimation.from_file(json_file) as anim: 93 | frame = anim.lottie_animation_get_frame_at_pos(0.5) 94 | 95 | assert frame == 25 96 | 97 | 98 | def test_lottie_animation_render(): 99 | with LottieAnimation.from_file(json_file) as anim: 100 | buffer = anim.lottie_animation_render() 101 | 102 | assert isinstance(buffer, bytes) 103 | 104 | 105 | def test_lottie_animation_render_async(): 106 | with LottieAnimation.from_file(json_file) as anim: 107 | anim.lottie_animation_render_async() 108 | buffer = anim.lottie_animation_render_flush() 109 | 110 | assert isinstance(buffer, bytes) 111 | 112 | 113 | def test_lottie_animation_property_override(): 114 | with LottieAnimation.from_file(json_file) as anim: 115 | anim.lottie_animation_property_override( 116 | LottieAnimationProperty.LOTTIE_ANIMATION_PROPERTY_FILLCOLOR, 117 | "**.**.**", 118 | ctypes.c_double(1.0), 119 | ctypes.c_double(0.0), 120 | ctypes.c_double(0.0), 121 | ) 122 | 123 | 124 | def test_lottie_animation_get_markerlist(): 125 | with LottieAnimation.from_file(json_file) as anim: 126 | markerlist = anim.lottie_animation_get_markerlist() 127 | 128 | assert markerlist is None 129 | 130 | 131 | def test_lottie_configure_model_cache_size(): 132 | with LottieAnimation.from_file(json_file) as anim: 133 | anim.lottie_configure_model_cache_size(0) 134 | 135 | 136 | @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") 137 | def test_render_pillow_frame(): 138 | from PIL import Image 139 | 140 | with LottieAnimation.from_file(json_file) as anim: 141 | im = anim.render_pillow_frame() 142 | 143 | assert isinstance(im, Image.Image) 144 | 145 | 146 | @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") 147 | def test_save_frame(tmpdir: LocalPath): 148 | tmppath = os.path.join(tmpdir, "0.png") 149 | with LottieAnimation.from_file(json_file) as anim: 150 | anim.save_frame(tmppath) 151 | 152 | assert os.path.isfile(tmppath) 153 | 154 | 155 | def _test_save_animation(out: str): 156 | with LottieAnimation.from_file(json_file) as anim: 157 | anim.save_frame(out) 158 | 159 | assert os.path.isfile(out) 160 | 161 | 162 | @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") 163 | def test_save_animation_apng(tmpdir: LocalPath): 164 | tmppath = os.path.join(tmpdir, "0.apng") 165 | _test_save_animation(tmppath) 166 | 167 | 168 | @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") 169 | def test_save_animation_gif(tmpdir: LocalPath): 170 | tmppath = os.path.join(tmpdir, "0.gif") 171 | _test_save_animation(tmppath) 172 | 173 | 174 | @pytest.mark.skipif(PILLOW_LOADED is False, reason="Pillow not installed") 175 | @pytest.mark.skipif( 176 | platform.python_implementation() == "PyPy", reason="Pillow without webp support" 177 | ) 178 | def test_save_animation_webp(tmpdir: LocalPath): 179 | tmppath = os.path.join(tmpdir, "0.webp") 180 | _test_save_animation(tmppath) 181 | --------------------------------------------------------------------------------