├── .github └── workflows │ ├── mirror-to-codeberg.yaml │ └── test-lint.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── LAYERED_SPEC.md ├── LICENSE.md ├── README.md ├── convert.py ├── documentation └── reference │ ├── README.md │ └── layeredimage │ ├── index.md │ ├── io │ ├── common.md │ ├── gif.md │ ├── index.md │ ├── layered.md │ ├── lsr.md │ ├── ora.md │ ├── pdn.md │ ├── psd.md │ ├── tiff.md │ ├── webp.md │ └── xcf.md │ ├── layeredimage.md │ └── layergroup.md ├── layeredimage ├── __init__.py ├── io │ ├── __init__.py │ ├── common.py │ ├── gif.py │ ├── layered.py │ ├── lsr.py │ ├── ora.py │ ├── pdn.py │ ├── psd.py │ ├── tiff.py │ ├── webp.py │ └── xcf.py ├── layeredimage.py └── layergroup.py ├── pyproject.toml ├── readme-assets └── icons │ ├── LayeredImage.png │ ├── LayeredImage.xcf │ ├── name.png │ └── proj-icon.png ├── requirements.txt └── tests ├── README.md ├── __init__.py ├── data ├── gif_output.gif ├── gif_output.ora ├── gif_output.png ├── gif_output.webp ├── gif_output_expected.png ├── layered_image.gif ├── layered_image.layered ├── layered_image.layeredc ├── layered_image.lsr ├── layered_image.ora ├── layered_image.pdn ├── layered_image.psd ├── layered_image.tiff ├── layered_image.webp ├── layered_image.xcf ├── layered_image_expected.png ├── layered_image_expected_nh.png ├── layered_output.gif ├── layered_output.ora ├── layered_output.png ├── layered_output.tiff ├── layered_output.webp ├── layeredc_output.gif ├── layeredc_output.ora ├── layeredc_output.png ├── layeredc_output.tiff ├── layeredc_output.webp ├── lsr_output.lsr ├── lsr_output.ora ├── lsr_output.png ├── lsr_output.webp ├── ora_output.gif ├── ora_output.layered ├── ora_output.layeredc ├── ora_output.ora ├── ora_output.png ├── ora_output.tiff ├── ora_output.webp ├── pdn_output.gif ├── pdn_output.ora ├── pdn_output.png ├── pdn_output.tiff ├── pdn_output.webp ├── psd_output.gif ├── psd_output.ora ├── psd_output.png ├── psd_output.tiff ├── psd_output.webp ├── tiff_output.gif ├── tiff_output.ora ├── tiff_output.png ├── tiff_output.tiff ├── tiff_output.webp ├── webp_output.gif ├── webp_output.ora ├── webp_output.png ├── webp_output.webp ├── xcf_output.gif ├── xcf_output.ora ├── xcf_output.png ├── xcf_output.tiff └── xcf_output.webp ├── test_layeredimage.py ├── test_layergroup.py └── test_main.py /.github/workflows/mirror-to-codeberg.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Sync repo to the Codeberg mirror 3 | name: Repo sync GitHub -> Codeberg 4 | on: 5 | push: 6 | branches: 7 | - '**' 8 | 9 | jobs: 10 | codeberg: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | - uses: spyoungtech/mirror-action@v0.7.0 17 | with: 18 | REMOTE: "https://codeberg.org/FredHappyface/LayeredImage.git" 19 | GIT_USERNAME: FredHappyface 20 | GIT_PASSWORD: ${{ secrets.CODEBERG_PASSWORD }} 21 | -------------------------------------------------------------------------------- /.github/workflows/test-lint.yaml: -------------------------------------------------------------------------------- 1 | name: Python Test and Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | name: Python Test and Lint 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | python-version: 19 | - '3.9' 20 | - '3.10' 21 | - '3.11' 22 | - '3.12' 23 | - '3.13' 24 | 25 | steps: 26 | - name: Checkout code 27 | uses: actions/checkout@v4 28 | 29 | - name: Set up Python ${{ matrix.python-version }} 30 | uses: actions/setup-python@v4 31 | with: 32 | python-version: ${{ matrix.python-version }} 33 | 34 | - name: Install UV 35 | run: | 36 | curl -LsSf https://astral.sh/uv/install.sh | sh 37 | 38 | - name: Install dependencies 39 | run: uv sync 40 | 41 | - name: Run pytest 42 | run: uv run pytest 43 | 44 | - name: Run ruff 45 | run: uv run ruff check --output-format=github 46 | continue-on-error: true 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !test 2 | uv.lock 3 | profile.* 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 102 | __pypackages__/ 103 | 104 | # Celery stuff 105 | celerybeat-schedule 106 | celerybeat.pid 107 | 108 | # SageMath parsed files 109 | *.sage.py 110 | 111 | # Environments 112 | .env 113 | .venv 114 | env/ 115 | venv/ 116 | ENV/ 117 | env.bak/ 118 | venv.bak/ 119 | 120 | # Spyder project settings 121 | .spyderproject 122 | .spyproject 123 | 124 | # Rope project settings 125 | .ropeproject 126 | 127 | # mkdocs documentation 128 | /site 129 | 130 | # mypy 131 | .mypy_cache/ 132 | .dmypy.json 133 | dmypy.json 134 | 135 | # Pyre type checker 136 | .pyre/ 137 | 138 | # pytype static type analyzer 139 | .pytype/ 140 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.9.6 4 | hooks: 5 | - id: ruff 6 | args: [ --fix ] 7 | - id: ruff-format 8 | 9 | - repo: https://github.com/RobertCraigie/pyright-python 10 | rev: v1.1.394 11 | hooks: 12 | - id: pyright 13 | 14 | - repo: local 15 | hooks: 16 | - id: generate requirements 17 | name: generate requirements 18 | entry: uv export --no-hashes --no-dev -o requirements.txt 19 | language: system 20 | pass_filenames: false 21 | - id: safety 22 | name: safety 23 | entry: uv run safety 24 | language: system 25 | pass_filenames: false 26 | - id: make docs 27 | name: make docs 28 | entry: uv run handsdown --cleanup -o documentation/reference 29 | language: system 30 | pass_filenames: false 31 | - id: build package 32 | name: build package 33 | entry: uv build 34 | language: system 35 | pass_filenames: false 36 | - id: pytest 37 | name: pytest 38 | entry: uv run pytest 39 | language: system 40 | pass_filenames: false 41 | 42 | - repo: https://github.com/pre-commit/pre-commit-hooks 43 | rev: v5.0.0 44 | hooks: 45 | - id: trailing-whitespace 46 | - id: end-of-file-fixer 47 | - id: check-case-conflict 48 | - id: check-executables-have-shebangs 49 | - id: check-json 50 | - id: check-merge-conflict 51 | - id: check-shebang-scripts-are-executable 52 | - id: check-symlinks 53 | - id: check-toml 54 | - id: check-vcs-permalinks 55 | - id: check-yaml 56 | - id: detect-private-key 57 | - id: mixed-line-ending 58 | 59 | - repo: https://github.com/boidolr/pre-commit-images 60 | rev: v1.8.4 61 | hooks: 62 | - id: optimize-jpg 63 | - id: optimize-png 64 | - id: optimize-svg 65 | - id: optimize-webp 66 | 67 | exclude: "tests/data|documentation/reference" 68 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All major and minor version changes will be documented in this file. Details of 4 | patch-level version changes can be found in [commit messages](../../commits/master). 5 | 6 | ## 2024.3 - 2024/03/30 7 | 8 | - fix bug where dimensions auto-calculation disregards layer offsets https://github.com/FHPythonUtils/LayeredImage/issues/7 9 | 10 | ## 2024.2.1 - 2024/03/17 11 | 12 | - use absolute imports 13 | 14 | ## 2024.2 - 2024/01/27 15 | 16 | - bug fixes 17 | - use new blendmodes 18 | 19 | ## 2024.1 - 2024/01/17 20 | 21 | - update tooling 22 | - ruff 23 | - pyright 24 | - use loguru to replace print statements 25 | - use pathlib in place of os.path 26 | - absolute imports in the place of relative imports 27 | - Address “The Boolean Trap” with kw only args 28 | - More type hints 29 | - remove deprecated functions from `2021.2.4` 30 | 31 | ## 2024 - 2024/01/07 32 | 33 | - update dependencies 34 | 35 | ## 2023.0.2 - 2023/12/01 36 | 37 | - add: Using os.path.splitext() to support Path objects https://github.com/FHPythonUtils/LayeredImage/pull/6, thank you https://github.com/denilsonsa! 38 | 39 | ## 2023 - 2023/08/31 40 | 41 | - Update deps 42 | 43 | ## 2022.0.1 2022/04/06 44 | 45 | - Remove metprint 46 | - Move docs 47 | 48 | ## 2022 - 2022/01/23 49 | 50 | - Bump pillow version (CVE-2022-22815, CVE-2022-22816, CVE-2022-22817) 51 | - Update deps 52 | - Improve save layered image to gif functionality 53 | - Update directory structure for formal tests 54 | 55 | ## 2021.2.8 - 2021/11/07 56 | 57 | - add pre-commit 58 | - code quality improvements 59 | 60 | ## 2021.2.7 - 2021/10/02 61 | 62 | - code quality improvements 63 | 64 | ## 2021.2.6 - 2021/09/13 65 | 66 | - Update pillow 67 | 68 | ## 2021.2.4 - 2021/06/08 69 | 70 | - Update blend.py https://github.com/FHPythonUtils/LayeredImage/issues/3 71 | - reduce duplication with upstream libs 72 | 73 | ## 2021.2.3 - 2021/05/01 74 | 75 | - bugfix backwards compat 76 | 77 | ## 2021.2.2 - 2021/05/01 78 | 79 | - Updated formatting 80 | - Deprecated 'raster' functions and replaced with more accurate naming 81 | - Improved docs 82 | 83 | ## 2021.2.1 - 2021/03/18 84 | 85 | - Update Pillow >= 8.1.1 due to high severity security vulnerabilities: 86 | - CVE-2021-27923 87 | - CVE-2020-35654 88 | - CVE-2020-35653 89 | - CVE-2021-27921 90 | - CVE-2021-27922 91 | - CVE-2020-35655 92 | 93 | ## 2021.2 - 2021.03.03 94 | 95 | - Improve MI score by refactoring `layeredimage.io.py` to `layeredimage.io/` 96 | 97 | ## 2021.1.1 - 2021.03.02 98 | 99 | - Try out new yapf formatting. 100 | - Tidy up 101 | - `pyupgrade` 102 | 103 | ## 2021.1 - 2021.01.20 104 | 105 | - `pypdn` compatible with py 3.9 so added back 106 | 107 | ## 2021 - 2021.01.18 108 | 109 | - Fix bug on python 3.7/3.8 110 | 111 | ## 2020.7 - 2020/10/29 112 | 113 | - Using FHMake to build 114 | - Added type hinting 115 | - Dropped support for python < 3.7 116 | - Added support for python 3.9 117 | - pypdn has been dropped until it is compatible with python 3.9 118 | - replaced `psd-tools3` with `psdtoolsx` 119 | 120 | ## 2020.6.5 - 2020/06/15 121 | 122 | - Fix ora write bug causing the layers to be inverted (monkey patch) 123 | 124 | ## 2020.6.4 - 2020/05/14 125 | 126 | - Fix png minifier for layeredc 127 | - Fix fatal read xcf bug (upstream) 128 | - Update pyora (security benefits and removal of save fix) 129 | 130 | ## 2020.6.3 - 2020/05/13 131 | 132 | - Fix bug due to spelling error 133 | 134 | ## 2020.6.2 - 2020/05/10 135 | 136 | - Removed redundant function 137 | - File extensions can now be case-insensitive 138 | 139 | ## 2020.6.1 - 2020/05/08 140 | 141 | - Minor change to stack.json for .layered (no longer minified) 142 | - Added .layeredc that will attempt to optimize the image. Savings of 10 - 20% 143 | 144 | ## 2020.6 - 2020/05/08 145 | 146 | - Added .layered image spec and implementation to store blendmodes that are 147 | not supported by ora and some other formats that this lib can write too. 148 | Still, use ora whenever possible as this is a more supported image format. 149 | - Updates to `gimpformats` mean that visibility is now correctly preserved. 150 | - Optimizations to save functions 151 | - Added json functions to get data as a dictionary 152 | 153 | ## 2020.5.6 - 2020/05/06 154 | 155 | - Updated classifiers 156 | 157 | ## 2020.5.5 - 2020/05/03 158 | 159 | - Upgraded from `gimpformats_unofficial` to `gimpformats` 160 | - Disable some pylint errors for snippets 161 | 162 | ## 2020.5.4 - 2020/05/02 163 | 164 | - Bugfix openLayer_XCF: layer offsets in a group are now correct 165 | 166 | ## 2020.5.3 - 2020/05/02 167 | 168 | - Added full support for Open Raster Image blend modes 169 | 170 | ## 2020.5.2 - 2020/05/01 171 | 172 | - Added PINLIGHT, VIVIDLIGHT, EXCLUSION 173 | - Moved blending heavy lifting to a shiny new library 174 | 175 | ## 2020.5.1 - 2020/04/29 176 | 177 | - Added blend modes from pyora (credited in file docstring): GRAINEXTRACT, 178 | GRAINMERGE, DIVIDE, HUE, SATURATION, COLOUR, LUMINOSITY 179 | 180 | ## 2020.5 - 2020/04/28 181 | 182 | - Added LSR support 183 | - Added make.py 184 | - Bugfix pdn layer offset in group 185 | - Print more descriptive LayerImage 186 | - Bugfixes to flattenLayerOrGroup 187 | 188 | ## 2020.4.2 - 2020/04/27 189 | 190 | - Update documentation (again) using pydoc-markdown 3 191 | 192 | ## 2020.4.1 - 2020/04/26 193 | 194 | - Update documentation 195 | 196 | ## 2020.4 - 2020/04/26 197 | 198 | - Added GIF and WEBP support 199 | - Automate tests 200 | 201 | ## 2020.3.3 - 2020/04/25 202 | 203 | - Updated blend.py to map blend types to functions, rather than use a series of 204 | if statements 205 | 206 | ## 2020.3.2 - 2020/04/24 207 | 208 | - Modules are no longer optional as this will create excess crashes for what is 209 | four additional dependencies 210 | 211 | ## 2020.3.1 - 2020/04/24 212 | 213 | - Fix ResourceWarning when opening a file 214 | 215 | ## 2020.3 - 2020/04/24 216 | 217 | - Fixed bug that caused hidden layers to be rendered by default 218 | - Using pypdn 1.05 - PDNs work again on python 3.8 219 | 220 | ## 2020.2.1 - 2020/04/24 221 | 222 | - Fixed incorrect docstrings 223 | - Added SOFTLIGHT and HARDLIGHT 224 | 225 | ## 2020.2 - 2020/04/23 226 | 227 | - Added basic support for blend modes NORMAL, MULTIPLY, ADDITIVE, COLOURBURN, 228 | COLOURDODGE, REFLECT, GLOW, OVERLAY, DIFFERENCE, NEGATION, LIGHTEN, DARKEN, 229 | SCREEN, XOR 230 | - Python 3.5 is no longer supported 231 | 232 | ## 2020.1.1 - 2020/04/22 233 | 234 | - Export to a flat image 235 | - Bugfix: If the file does not exist throws an error instead of exiting 236 | 237 | ## 2020.1 - 2020/04/22 238 | 239 | - Added TIFF support 🎉 240 | - Discovered that my patch for `pypdn` cannot be applied so throw an error and 241 | message for python 3.8+ (waiting on upstream) 😭 - Therefore no test output for 242 | these 243 | - Better error for unrecognised extension 244 | - Fixes to saving .ora files. 245 | - Groups are saved with an offset rather than relying on `project.add_layer()`. 246 | - Fixed `save_ORA_fix` to save images without additional padding 247 | - Updates to README 248 | - `LayeredImage.extractLayers` will now 'raster' layers in a group to deal with 249 | offsets, dimensions etc 250 | 251 | ## 2020 - 2020/04/20 252 | 253 | - First release 254 | -------------------------------------------------------------------------------- /LAYERED_SPEC.md: -------------------------------------------------------------------------------- 1 | # .layered 2 | 3 | .layered is highly inspired by the open raster format and aims to provide an 4 | exchange format in the cases when saving in ora would cause unacceptable data 5 | loss. .layered has been designed so that if the format became deprecated and no 6 | readers existed for it tomorrow, the data would be easily salvageable. 7 | 8 | ## Rationale 9 | 10 | .ora is sufficient for the vast majority of layered images that are represented 11 | by this library and should be used where possible. However, some blending 12 | functions that are present in other formats such as xcf and psd are unable to 13 | be stored in .ora. Meaning that yet another file format is required. 14 | 15 | Ora supports the following blend modes: 16 | 17 | ```none 18 | NORMAL 19 | MULTIPLY 20 | COLOURBURN 21 | COLOURDODGE 22 | REFLECT 23 | OVERLAY 24 | DIFFERENCE 25 | LIGHTEN 26 | DARKEN 27 | SCREEN 28 | HARDLIGHT 29 | SOFTLIGHT 30 | HUE 31 | SATURATION 32 | COLOUR 33 | LUMINOSITY 34 | ADDITIVE 35 | DESTIN 36 | DESTOUT 37 | DESTATOP 38 | SRCATOP 39 | ``` 40 | 41 | ## When to use .layered 42 | 43 | Ideally .layered should only be used when it is not possible to store the 44 | layered image in another format that is more readily exchangeable due to 45 | unacceptable data loss. .layered has been designed so that if the format 46 | became deprecated and no readers existed for it tomorrow, the data would be 47 | easily salvageable. 48 | 49 | As previously stated it is far better to use ora whenever possible 50 | 51 | However, if you make use of the following blendmodes .layered is preferable 52 | 53 | ```none 54 | GLOW 55 | NEGATION 56 | XOR 57 | GRAINEXTRACT 58 | GRAINMERGE 59 | DIVIDE 60 | PINLIGHT 61 | VIVIDLIGHT 62 | EXCLUSION 63 | ``` 64 | 65 | ## Advantages 66 | 67 | .layered is able to store blend modes that cannot be stored in an open raster 68 | image or other formats. 69 | 70 | ## Drawbacks 71 | 72 | This is another format in a sea of formats and is only recommended to be used 73 | where it is not possible to use another format for this reason. Graphics editor 74 | support for .layered is unlikely unless this became an extremely popular format 75 | which given the existence of ora isn't very likely. 76 | 77 | ## Versions 78 | 79 | From 2020.2 to 2020.6 the only changes in the layered image object has been the 80 | addition of more blending modes. Other elements have remained the same (2020.2 81 | added blend modes and so there was a change to support these). Therefore, 82 | aside from the potential for more blend modes to be added in the future, this 83 | spec shouldn't change 84 | 85 | ### 2020 86 | 87 | ## Structure 88 | 89 | Based very heavily upon the open raster specification. A .layered file is 90 | basically a zip file containing data on the layers and groups such as the name, 91 | offsets, the images stored as png and a composite image that can be extracted 92 | by file managers/ viewers. 93 | 94 | ```none 95 | example.layered 96 | ├ stack.json 97 | ├ data/ 98 | │ └ [image data files referenced by stack.json, typically [layername].png] 99 | ├ thumbnail.png 100 | └ composite.png 101 | ``` 102 | 103 | ## Required files 104 | 105 | The files except for composite.png are required though composite.png is 106 | highly recommended to be included 107 | 108 | ### stack.json 109 | 110 | stack.json contains all text data on a layer or group such as the name, 111 | dimensions and offsets. 112 | 113 | #### Why json? 114 | 115 | Json has been used as it is supported as part of a core python installation. 116 | A large number of json parsers are available. It is used by the likes of 117 | GraphQl. It's good enough for Apple LSR. 118 | 119 | ### data/\[layername\].png 120 | 121 | Layer image data should be stored here. 122 | 123 | ## Optional files 124 | 125 | ### composite.png 126 | 127 | Composite image that can be extracted by file managers/ viewers. This must 128 | be representative of the .layered image for example hidden layers are not shown 129 | as part of this image. 130 | 131 | ### thumbnail.png 132 | 133 | Composite image that can be extracted by file managers/ viewers. This must 134 | be representative of the .layered image for example hidden layers are not shown 135 | as part of this image and have a maximum size of 256x256 pixels. 136 | 137 | ## Comparison to ORA 138 | 139 | Here is a snippet of the ora spec for the structure: 140 | 141 | ```none 142 | example.ora [considered as a folder-like object] 143 | ├ mimetype 144 | ├ stack.xml 145 | ├ data/ 146 | │ ├ [image data files referenced by stack.xml, typically layer*.png] 147 | │ └ [other data files, indexed elsewhere] 148 | ├ Thumbnails/ 149 | │ └ thumbnail.png 150 | └ mergedimage.png 151 | ``` 152 | 153 | ### Similarities 154 | 155 | The file format is essentially a list of images, a composite image and a file 156 | containing information on the layers and groups (name, size, offsets) 157 | 158 | ### Differences 159 | 160 | .layered lacks the mimetype. This is because the file type can 161 | and should be determined from the extension for .layered 162 | 163 | Layers are identified by the layer name: eg a layer called 'example layer' would 164 | have an image named 'example layer.png'. 165 | 166 | Json is used instead of xml. 167 | 168 | The thumbnail is in the root directory as multiple thumbnails are not planned. 169 | 170 | ## Stack data 171 | 172 | ### Image 173 | 174 | ```none 175 | dimensions: (int, int) 176 | layersAndGroups: [layer|group] 177 | ``` 178 | 179 | ### Group 180 | 181 | ```none 182 | name: string 183 | offsets: (int, int) 184 | opacity: float 185 | visible: float 186 | dimensions: (int, int) 187 | type: GROUP 188 | blendmode: BlendTypes.[] 189 | layers: [layer] 190 | ``` 191 | 192 | ### Layer 193 | 194 | ```none 195 | name: string 196 | offsets: (int, int) 197 | opacity: float 198 | visible: float 199 | dimensions: (int, int) 200 | type: LAYER 201 | blendmode: BlendTypes.[] 202 | image: PIL.Image 203 | ``` 204 | 205 | Consider an image that has the following structure: 206 | 207 | Bottom layer 208 | 209 | ```none 210 | Group ("group_0") 211 | Layer("layer_0") 212 | Layer("layer_1") 213 | Layer("layer_2") 214 | ``` 215 | 216 | Top Layer 217 | 218 | The stack.json would look something like this: 219 | 220 | ```none 221 | {"dimensions": [640, 640], "layersAndGroups": [ 222 | {"name": "group_0", "offsets": [0, 0], "opacity": 1.0, "visible": true, 223 | "dimensions": [640, 640], "type": "GROUP", "blendmode": "NORMAL", 224 | "layers": [ 225 | {"name": "layer_0", "offsets": [100, 0], "opacity": 1.0, "visible": true, 226 | "dimensions": [640, 640], "type": "LAYER", "blendmode": "NORMAL"}, 227 | {"name": "layer_1", "offsets": [100, 0], "opacity": 1.0, "visible": true, 228 | "dimensions": [640, 640], "type": "LAYER", "blendmode": "NORMAL"} 229 | ]}, 230 | {"name": "layer_2", "offsets": [295, 292], "opacity": 1.0, "visible": false, 231 | "dimensions": [250, 250], "type": "LAYER", "blendmode": "NORMAL"}]} 232 | ``` 233 | 234 | ## .layeredc 235 | 236 | This is a compressed version of the layered format. 237 | 238 | Changes: 239 | 240 | - The .zip file is compressed with 'deflate' 241 | - .PNGs are quantized where possible 242 | - stack.json is minified 243 | 244 | This makes anywhere between 10 - 20% file reduction in testing. In theory this 245 | may get as high as 60% (quantized images can be 1/3 of the size of unoptimized 246 | pngs) 247 | 248 | During testing this seems on par with the file size of xcf. 249 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2020 Kieran BW 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub top language](https://img.shields.io/github/languages/top/FHPythonUtils/LayeredImage.svg?style=for-the-badge&cacheSeconds=28800)](../../) 2 | [![Issues](https://img.shields.io/github/issues/FHPythonUtils/LayeredImage.svg?style=for-the-badge&cacheSeconds=28800)](../../issues) 3 | [![License](https://img.shields.io/github/license/FHPythonUtils/LayeredImage.svg?style=for-the-badge&cacheSeconds=28800)](/LICENSE.md) 4 | [![Commit activity](https://img.shields.io/github/commit-activity/m/FHPythonUtils/LayeredImage.svg?style=for-the-badge&cacheSeconds=28800)](../../commits/master) 5 | [![Last commit](https://img.shields.io/github/last-commit/FHPythonUtils/LayeredImage.svg?style=for-the-badge&cacheSeconds=28800)](../../commits/master) 6 | [![PyPI Downloads](https://img.shields.io/pypi/dm/layeredimage.svg?style=for-the-badge&cacheSeconds=28800)](https://pypistats.org/packages/layeredimage) 7 | [![PyPI Total Downloads](https://img.shields.io/badge/dynamic/json?style=for-the-badge&label=total%20downloads&query=%24.total_downloads&url=https%3A%2F%2Fapi%2Epepy%2Etech%2Fapi%2Fv2%2Fprojects%2Flayeredimage)](https://pepy.tech/project/layeredimage) 8 | [![PyPI Version](https://img.shields.io/pypi/v/layeredimage.svg?style=for-the-badge&cacheSeconds=28800)](https://pypi.org/project/layeredimage) 9 | 10 | 11 | # LayeredImage 12 | 13 | Project Icon 14 | 15 | Use this module to read, and write to a number of layered image formats 16 | 17 | - [Compatibility](#compatibility) 18 | - [Overview](#overview) 19 | - [Key](#key) 20 | - [Reading - Group](#reading---group) 21 | - [Reading - Layer](#reading---layer) 22 | - [Writing - Group](#writing---group) 23 | - [Writing - Layer](#writing---layer) 24 | - [.layered](#layered) 25 | - [Example Usage](#example-usage) 26 | - [Documentation](#documentation) 27 | - [Install With PIP](#install-with-pip) 28 | - [Language information](#language-information) 29 | - [Built for](#built-for) 30 | - [Install Python on Windows](#install-python-on-windows) 31 | - [Chocolatey](#chocolatey) 32 | - [Windows - Python.org](#windows---pythonorg) 33 | - [Install Python on Linux](#install-python-on-linux) 34 | - [Apt](#apt) 35 | - [Dnf](#dnf) 36 | - [Install Python on MacOS](#install-python-on-macos) 37 | - [Homebrew](#homebrew) 38 | - [MacOS - Python.org](#macos---pythonorg) 39 | - [How to run](#how-to-run) 40 | - [Windows](#windows) 41 | - [Linux/ MacOS](#linux-macos) 42 | - [Building](#building) 43 | - [Testing](#testing) 44 | - [Download Project](#download-project) 45 | - [Clone](#clone) 46 | - [Using The Command Line](#using-the-command-line) 47 | - [Using GitHub Desktop](#using-github-desktop) 48 | - [Download Zip File](#download-zip-file) 49 | - [Community Files](#community-files) 50 | - [Licence](#licence) 51 | - [Changelog](#changelog) 52 | - [Code of Conduct](#code-of-conduct) 53 | - [Contributing](#contributing) 54 | - [Security](#security) 55 | - [Support](#support) 56 | - [Rationale](#rationale) 57 | 58 | ## Compatibility 59 | 60 | Bear in mind that the tables below may not be completely accurate. If that is 61 | the case, please open an issue and I will fix the tables. 62 | 63 | ### Overview 64 | 65 | #### Key 66 | 67 | - ✔ - Supported 68 | - ⚠ - Things will look the same, but data is lost 69 | - ❌ - This is not supported and will cause loss of data 70 | - N/A - The source format does not support this so treat this as a ✔ 71 | 72 | | Format | .ora | .pdn | .xcf | .psd | .tiff/ .tif | .webp | .gif | .lsr | 73 | | ------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | 74 | | Read | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | 75 | | Layers | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | 76 | | Groups | ✔ | N/A | ✔ | ✔ | N/A | N/A | N/A | ✔ | 77 | | Write | ✔ | ❌ | ❌ | ❌ | ⚠ | ⚠ | ⚠ | ✔ | 78 | 79 | #### Reading - Group 80 | 81 | | Format | .ora | .pdn | .xcf | .psd | .tiff/ .tif | .webp | .gif | .lsr | 82 | | ---------- | ------------------ | ---- | ------------------ | ------------------ | ----------- | ----- | ---- | ------------------ | 83 | | Name | ✔ | N/A | ✔ | ✔ | N/A | N/A | N/A | ✔ | 84 | | Dimensions | ⚠ | N/A | ✔ | ✔ | N/A | N/A | N/A | ✔ | 85 | | Offsets | ✔ | N/A | ✔ | ✔ | N/A | N/A | N/A | ✔ | 86 | | Opacity | ✔ | N/A | ✔ | ✔ | N/A | N/A | N/A | N/A | 87 | | Visibility | ✔ | N/A | ✔ | ✔ | N/A | N/A | N/A | N/A | 88 | | Blend Mode | ✔ | N/A | ✔ | ✔ | N/A | N/A | N/A | N/A | 89 | 90 | #### Reading - Layer 91 | 92 | | Format | .ora | .pdn | .xcf | .psd | .tiff/ .tif | .webp | .gif | .lsr | 93 | | ---------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | 94 | | Name | ✔ | ✔ | ✔ | ✔ | ✔ | ⚠ | ⚠ | ✔ | 95 | | Dimensions | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | 96 | | Offsets | ✔ | N/A | ✔ | ✔ | ✔ | N/A | N/A | N/A | 97 | | Opacity | ✔ | ✔ | ✔ | ✔ | N/A | N/A | N/A | N/A | 98 | | Visibility | ✔ | ✔ | ✔ | ✔ | N/A | N/A | N/A | N/A | 99 | | Blend Mode | ✔ | ✔ | ✔ | ✔ | N/A | N/A | N/A | N/A | 100 | 101 | #### Writing - Group 102 | 103 | | Format | .ora | .pdn | .xcf | .psd | .tiff/ .tif | .webp | .gif | .lsr | 104 | | ---------- | ------------------ | ---- | ---- | ---- | ----------- | --------- | --------- | ------------------ | 105 | | Name | ✔ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✔ | 106 | | Dimensions | ✔ | ❌ | ❌ | ❌ | ⚠ | ⚠ | ⚠ | ✔ | 107 | | Offsets | ✔ | ❌ | ❌ | ❌ | ⚠ | ⚠ | ⚠ | ✔ | 108 | | Opacity | ✔ | ❌ | ❌ | ❌ | ⚠ | ⚠ | ⚠ | ⚠ | 109 | | Visibility | ✔ | ❌ | ❌ | ❌ | ⚠ | ⚠ | ⚠ | ⚠ | 110 | | Blend Mode | ✔ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 111 | 112 | ```none 113 | Layers are extracted from groups and saved to TIFF/ GIF or WEBP 114 | ``` 115 | 116 | #### Writing - Layer 117 | 118 | | Format | .ora | .pdn | .xcf | .psd | .tiff/ .tif | .webp | .gif | .lsr | 119 | | ---------- | ------------------ | ---- | ---- | ---- | ----------- | --------- | --------- | --------- | 120 | | Name | ✔ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ⚠ | 121 | | Dimensions | ✔ | ❌ | ❌ | ❌ | ⚠ | ⚠ | ⚠ | ⚠ | 122 | | Offsets | ✔ | ❌ | ❌ | ❌ | ⚠ | ⚠ | ⚠ | ⚠ | 123 | | Opacity | ✔ | ❌ | ❌ | ❌ | ⚠ | ⚠ | ⚠ | ⚠ | 124 | | Visibility | ✔ | ❌ | ❌ | ❌ | ⚠ | ⚠ | ⚠ | ⚠ | 125 | | Blend Mode | ✔ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 126 | 127 | ```none 128 | Layers are rendered with offsets before being written to TIFF/ GIF or WEBP 129 | First child layers are placed in a group when written to LSR 130 | ``` 131 | 132 | ## .layered 133 | 134 | .layered is highly inspired by the open raster format and aims to provide an 135 | exchange format in the cases when saving in ora would cause unacceptable data 136 | loss. .layered has been designed so that if the format became deprecated and no 137 | readers existed for it tomorrow, the data would be easily salvageable. 138 | 139 | See the [LAYERED_SPEC](/LAYERED_SPEC.md) for more information. 140 | 141 | ## Example Usage 142 | 143 | Here's some basic example usage below. 144 | 145 | ```python 146 | """Example module """ 147 | from pathlib import Path 148 | THISDIR = str(Path(__file__).resolve().parent) 149 | import layeredimage.io 150 | 151 | # Do stuff 152 | ora = layeredimage.io.openLayerImage(f"{THISDIR}/image.ora") 153 | 154 | imageDimensions = ora.dimensions 155 | # There are a load of handy functions for getting layers, and adding new 156 | # layers, but here we will act directly on the object 157 | layer = ora.layersAndGroups[0] # For the sake of the e.g. this is a layer 158 | 159 | # Lets overwrite the layer with a transparent image (bit boring I know...) 160 | layer.image = Image.new("RGBA", imageDimensions) 161 | ora.layersAndGroups[0] = layer 162 | 163 | # And let's save 164 | layeredimage.io.saveLayerImage(f"{THISDIR}/image(modified).ora", ora) 165 | 166 | # Let's save a flattened version too 167 | ora.getFlattenLayers().save(f"{THISDIR}/image(modified).png") 168 | 169 | # Doing stuff with a group 170 | group = ora.getLayerOrGroup(1) # For the sake of the e.g. this is a group 171 | group.layers[0].image.show() # Open the image of the first layer of the group 172 | 173 | # Deleting a layer/ group 174 | ora.removeLayerOrGroup(2) 175 | ``` 176 | 177 | Images are PIL.Image (s) and so you can use the power of Pillow to apply 178 | filters, and other modifications to the images. 179 | 180 | See below for an old version of the tests. These provide a few examples of 181 | file conversions. Not going to get 100% coverage anytime soon but hopefully 182 | this will help a little. 183 | 184 | ```python 185 | """Test module """ 186 | 187 | import sys 188 | import os 189 | from pathlib import Path 190 | THISDIR = str(Path(__file__).resolve().parent) 191 | sys.path.insert(0, os.path.dirname(THISDIR)) 192 | import layeredimage.io 193 | 194 | # ORA 195 | ora = layeredimage.io.openLayerImage(f"{THISDIR}/base24.ora") 196 | layeredimage.io.saveLayerImage(f"{THISDIR}/base24(ora).ora", ora) 197 | layeredimage.io.saveLayerImage(f"{THISDIR}/base24(ora).tiff", ora) 198 | ora.getFlattenLayers().save(f"{THISDIR}/base24(ora).png") 199 | 200 | # PSD 201 | psd = layeredimage.io.openLayerImage(f"{THISDIR}/base24.psd") 202 | layeredimage.io.saveLayerImage(f"{THISDIR}/base24(psd).ora", psd) 203 | layeredimage.io.saveLayerImage(f"{THISDIR}/base24(psd).tiff", psd) 204 | psd.getFlattenLayers().save(f"{THISDIR}/base24(psd).png") 205 | 206 | # PDN 207 | pdn = layeredimage.io.openLayerImage(f"{THISDIR}/base24.pdn") 208 | layeredimage.io.saveLayerImage(f"{THISDIR}/base24(pdn).ora", pdn) 209 | layeredimage.io.saveLayerImage(f"{THISDIR}/base24(pdn).tiff", pdn) 210 | pdn.getFlattenLayers().save(f"{THISDIR}/base24(pdn).png") 211 | 212 | # XCF 213 | xcf = layeredimage.io.openLayerImage(f"{THISDIR}/base24.xcf") 214 | layeredimage.io.saveLayerImage(f"{THISDIR}/base24(xcf).ora", xcf) 215 | layeredimage.io.saveLayerImage(f"{THISDIR}/base24(xcf).tiff", xcf) 216 | xcf.getFlattenLayers().save(f"{THISDIR}/base24(xcf).png") 217 | 218 | # TIFF 219 | tiff = layeredimage.io.openLayerImage(f"{THISDIR}/base24.tiff") 220 | layeredimage.io.saveLayerImage(f"{THISDIR}/base24(tiff).ora", tiff) 221 | layeredimage.io.saveLayerImage(f"{THISDIR}/base24(tiff).tiff", tiff) 222 | tiff.getFlattenLayers().save(f"{THISDIR}/base24(tiff).png") 223 | ``` 224 | 225 | ## Documentation 226 | 227 | A high-level overview of how the documentation is organized organized will help you know 228 | where to look for certain things: 229 | 230 | 234 | - The [Technical Reference](/documentation/reference) documents APIs and other aspects of the 235 | machinery. This documentation describes how to use the classes and functions at a lower level 236 | and assume that you have a good high-level understanding of the software. 237 | 241 | 242 | ## Install With PIP 243 | 244 | ```python 245 | pip install layeredimage 246 | ``` 247 | 248 | Head to https://pypi.org/project/layeredimage/ for more info 249 | 250 | ## Language information 251 | 252 | ### Built for 253 | 254 | This program has been written for Python versions 3.8 - 3.11 and has been tested with both 3.8 and 255 | 3.11 256 | 257 | ## Install Python on Windows 258 | 259 | ### Chocolatey 260 | 261 | ```powershell 262 | choco install python 263 | ``` 264 | 265 | ### Windows - Python.org 266 | 267 | To install Python, go to https://www.python.org/downloads/windows/ and download the latest 268 | version. 269 | 270 | ## Install Python on Linux 271 | 272 | ### Apt 273 | 274 | ```bash 275 | sudo apt install python3.x 276 | ``` 277 | 278 | ### Dnf 279 | 280 | ```bash 281 | sudo dnf install python3.x 282 | ``` 283 | 284 | ## Install Python on MacOS 285 | 286 | ### Homebrew 287 | 288 | ```bash 289 | brew install python@3.x 290 | ``` 291 | 292 | ### MacOS - Python.org 293 | 294 | To install Python, go to https://www.python.org/downloads/macos/ and download the latest 295 | version. 296 | 297 | ## How to run 298 | 299 | ### Windows 300 | 301 | - Module 302 | `py -3.x -m [module]` or `[module]` (if module installs a script) 303 | 304 | - File 305 | `py -3.x [file]` or `./[file]` 306 | 307 | ### Linux/ MacOS 308 | 309 | - Module 310 | `python3.x -m [module]` or `[module]` (if module installs a script) 311 | 312 | - File 313 | `python3.x [file]` or `./[file]` 314 | 315 | ## Building 316 | 317 | This project uses https://github.com/FHPythonUtils/FHMake to automate most of the building. This 318 | command generates the documentation, updates the requirements.txt and builds the library artefacts 319 | 320 | Note the functionality provided by fhmake can be approximated by the following 321 | 322 | ```sh 323 | handsdown --cleanup -o documentation/reference 324 | poetry export -f requirements.txt --output requirements.txt 325 | poetry export -f requirements.txt --with dev --output requirements_optional.txt 326 | poetry build 327 | ``` 328 | 329 | `fhmake audit` can be run to perform additional checks 330 | 331 | ## Testing 332 | 333 | For testing with the version of python used by poetry use 334 | 335 | ```sh 336 | poetry run pytest 337 | ``` 338 | 339 | Alternatively use `tox` to run tests over python 3.8 - 3.11 340 | 341 | ```sh 342 | tox 343 | ``` 344 | 345 | ## Download Project 346 | 347 | ### Clone 348 | 349 | #### Using The Command Line 350 | 351 | 1. Press the Clone or download button in the top right 352 | 2. Copy the URL (link) 353 | 3. Open the command line and change directory to where you wish to 354 | clone to 355 | 4. Type 'git clone' followed by URL in step 2 356 | 357 | ```bash 358 | git clone https://github.com/FHPythonUtils/LayeredImage 359 | ``` 360 | 361 | More information can be found at 362 | https://help.github.com/en/articles/cloning-a-repository 363 | 364 | #### Using GitHub Desktop 365 | 366 | 1. Press the Clone or download button in the top right 367 | 2. Click open in desktop 368 | 3. Choose the path for where you want and click Clone 369 | 370 | More information can be found at 371 | https://help.github.com/en/desktop/contributing-to-projects/cloning-a-repository-from-github-to-github-desktop 372 | 373 | ### Download Zip File 374 | 375 | 1. Download this GitHub repository 376 | 2. Extract the zip archive 377 | 3. Copy/ move to the desired location 378 | 379 | ## Community Files 380 | 381 | ### Licence 382 | 383 | MIT License 384 | Copyright (c) FredHappyface 385 | (See the [LICENSE](/LICENSE.md) for more information.) 386 | 387 | ### Changelog 388 | 389 | See the [Changelog](/CHANGELOG.md) for more information. 390 | 391 | ### Code of Conduct 392 | 393 | Online communities include people from many backgrounds. The *Project* 394 | contributors are committed to providing a friendly, safe and welcoming 395 | environment for all. Please see the 396 | [Code of Conduct](https://github.com/FHPythonUtils/.github/blob/master/CODE_OF_CONDUCT.md) 397 | for more information. 398 | 399 | ### Contributing 400 | 401 | Contributions are welcome, please see the 402 | [Contributing Guidelines](https://github.com/FHPythonUtils/.github/blob/master/CONTRIBUTING.md) 403 | for more information. 404 | 405 | ### Security 406 | 407 | Thank you for improving the security of the project, please see the 408 | [Security Policy](https://github.com/FHPythonUtils/.github/blob/master/SECURITY.md) 409 | for more information. 410 | 411 | ### Support 412 | 413 | Thank you for using this project, I hope it is of use to you. Please be aware that 414 | those involved with the project often do so for fun along with other commitments 415 | (such as work, family, etc). Please see the 416 | [Support Policy](https://github.com/FHPythonUtils/.github/blob/master/SUPPORT.md) 417 | for more information. 418 | 419 | ### Rationale 420 | 421 | The rationale acts as a guide to various processes regarding projects such as 422 | the versioning scheme and the programming styles used. Please see the 423 | [Rationale](https://github.com/FHPythonUtils/.github/blob/master/RATIONALE.md) 424 | for more information. 425 | -------------------------------------------------------------------------------- /convert.py: -------------------------------------------------------------------------------- 1 | """Example program to convert layered images to ora.""" 2 | 3 | from __future__ import annotations 4 | 5 | import contextlib 6 | from os import listdir 7 | from pathlib import Path 8 | 9 | import layeredimage.io 10 | 11 | THISDIR = Path(__file__).resolve().parent 12 | 13 | files = [THISDIR / file for file in listdir(THISDIR) if (THISDIR / file).is_file()] 14 | for file in files: 15 | with contextlib.suppress(ValueError): 16 | layeredimage.io.saveLayerImage(f"{file}.ora", layeredimage.io.openLayerImage(file)) 17 | -------------------------------------------------------------------------------- /documentation/reference/README.md: -------------------------------------------------------------------------------- 1 | # Layeredimage Index 2 | 3 | > Auto-generated documentation index. 4 | 5 | A full list of `Layeredimage` project modules. 6 | 7 | - [Layeredimage](layeredimage/index.md#layeredimage) 8 | - [Io](layeredimage/io/index.md#io) 9 | - [Common](layeredimage/io/common.md#common) 10 | - [Gif](layeredimage/io/gif.md#gif) 11 | - [Layered](layeredimage/io/layered.md#layered) 12 | - [Lsr](layeredimage/io/lsr.md#lsr) 13 | - [Ora](layeredimage/io/ora.md#ora) 14 | - [Pdn](layeredimage/io/pdn.md#pdn) 15 | - [Psd](layeredimage/io/psd.md#psd) 16 | - [Tiff](layeredimage/io/tiff.md#tiff) 17 | - [Webp](layeredimage/io/webp.md#webp) 18 | - [Xcf](layeredimage/io/xcf.md#xcf) 19 | - [LayeredImage](layeredimage/layeredimage.md#layeredimage) 20 | - [LayerGroup](layeredimage/layergroup.md#layergroup) 21 | -------------------------------------------------------------------------------- /documentation/reference/layeredimage/index.md: -------------------------------------------------------------------------------- 1 | # Layeredimage 2 | 3 | [Layeredimage Index](../README.md#layeredimage-index) / Layeredimage 4 | 5 | > Auto-generated documentation for [layeredimage](../../../layeredimage/__init__.py) module. 6 | 7 | - [Layeredimage](#layeredimage) 8 | - [Modules](#modules) 9 | 10 | ## Modules 11 | 12 | - [Io](io/index.md) 13 | - [LayeredImage](./layeredimage.md) 14 | - [LayerGroup](./layergroup.md) -------------------------------------------------------------------------------- /documentation/reference/layeredimage/io/common.md: -------------------------------------------------------------------------------- 1 | # Common 2 | 3 | [Layeredimage Index](../../README.md#layeredimage-index) / [Layeredimage](../index.md#layeredimage) / [Io](./index.md#io) / Common 4 | 5 | > Auto-generated documentation for [layeredimage.io.common](../../../../layeredimage/io/common.py) module. 6 | 7 | - [Common](#common) 8 | - [blendModeLookup](#blendmodelookup) 9 | - [expandLayer](#expandlayer) 10 | - [expandLayersToCanvas](#expandlayerstocanvas) 11 | 12 | ## blendModeLookup 13 | 14 | [Show source in common.py:15](../../../../layeredimage/io/common.py#L15) 15 | 16 | Get the blendmode from a lookup table. 17 | 18 | #### Signature 19 | 20 | ```python 21 | def blendModeLookup( 22 | blendmode: Any, 23 | blendLookup: dict[Any, Any], 24 | default: BlendType | tuple[str, ...] = BlendType.NORMAL, 25 | ) -> BlendType: ... 26 | ``` 27 | 28 | 29 | 30 | ## expandLayer 31 | 32 | [Show source in common.py:45](../../../../layeredimage/io/common.py#L45) 33 | 34 | #### Arguments 35 | 36 | ---- 37 | foreground (np.ndarray | Image.Image): The foreground layer (must be the same size as the background). 38 | - `opacity` *float, optional* - The opacity of the foreground image. Defaults to 1.0. 39 | offsets (Tuple[int, int], optional): Offsets for the foreground layer. Defaults to (0, 0). 40 | 41 | #### Returns 42 | 43 | ------- 44 | - `Image.Image` - The image. 45 | 46 | #### Signature 47 | 48 | ```python 49 | def expandLayer( 50 | dimensions: tuple[int, int], 51 | foreground: np.ndarray | Image.Image, 52 | opacity: float = 1.0, 53 | offsets: tuple[int, int] = (0, 0), 54 | ) -> Image.Image: ... 55 | ``` 56 | 57 | 58 | 59 | ## expandLayersToCanvas 60 | 61 | [Show source in common.py:27](../../../../layeredimage/io/common.py#L27) 62 | 63 | Return layers and throw a warning if the image has groups. 64 | 65 | #### Signature 66 | 67 | ```python 68 | def expandLayersToCanvas( 69 | layeredImage: LayeredImage, imageFormat: str 70 | ) -> list[Image.Image]: ... 71 | ``` 72 | 73 | #### See also 74 | 75 | - [LayeredImage](../layeredimage.md#layeredimage) -------------------------------------------------------------------------------- /documentation/reference/layeredimage/io/gif.md: -------------------------------------------------------------------------------- 1 | # Gif 2 | 3 | [Layeredimage Index](../../README.md#layeredimage-index) / [Layeredimage](../index.md#layeredimage) / [Io](./index.md#io) / Gif 4 | 5 | > Auto-generated documentation for [layeredimage.io.gif](../../../../layeredimage/io/gif.py) module. 6 | 7 | - [Gif](#gif) 8 | - [openLayer_GIF](#openlayer_gif) 9 | - [saveLayer_GIF](#savelayer_gif) 10 | 11 | ## openLayer_GIF 12 | 13 | [Show source in gif.py:13](../../../../layeredimage/io/gif.py#L13) 14 | 15 | Open a .gif file into a layered image. 16 | 17 | #### Signature 18 | 19 | ```python 20 | def openLayer_GIF(file: str) -> LayeredImage: ... 21 | ``` 22 | 23 | #### See also 24 | 25 | - [LayeredImage](../layeredimage.md#layeredimage) 26 | 27 | 28 | 29 | ## saveLayer_GIF 30 | 31 | [Show source in gif.py:31](../../../../layeredimage/io/gif.py#L31) 32 | 33 | Save a layered image as .gif. 34 | 35 | #### Signature 36 | 37 | ```python 38 | def saveLayer_GIF(fileName: str, layeredImage: LayeredImage) -> None: ... 39 | ``` 40 | 41 | #### See also 42 | 43 | - [LayeredImage](../layeredimage.md#layeredimage) -------------------------------------------------------------------------------- /documentation/reference/layeredimage/io/index.md: -------------------------------------------------------------------------------- 1 | # Io 2 | 3 | [Layeredimage Index](../../README.md#layeredimage-index) / [Layeredimage](../index.md#layeredimage) / Io 4 | 5 | > Auto-generated documentation for [layeredimage.io](../../../../layeredimage/io/__init__.py) module. 6 | 7 | - [Io](#io) 8 | - [exportFlatImage](#exportflatimage) 9 | - [extNotRecognised](#extnotrecognised) 10 | - [openLayerImage](#openlayerimage) 11 | - [saveLayerImage](#savelayerimage) 12 | - [Modules](#modules) 13 | 14 | ## exportFlatImage 15 | 16 | [Show source in __init__.py:114](../../../../layeredimage/io/__init__.py#L114) 17 | 18 | Export the layered image to a unilayer image file. 19 | 20 | #### Signature 21 | 22 | ```python 23 | def exportFlatImage(fileName: str, layeredImage: LayeredImage) -> None: ... 24 | ``` 25 | 26 | #### See also 27 | 28 | - [LayeredImage](../layeredimage.md#layeredimage) 29 | 30 | 31 | 32 | ## extNotRecognised 33 | 34 | [Show source in __init__.py:26](../../../../layeredimage/io/__init__.py#L26) 35 | 36 | Output the file extension not recognised error. 37 | 38 | #### Signature 39 | 40 | ```python 41 | def extNotRecognised(fileName: str) -> None: ... 42 | ``` 43 | 44 | 45 | 46 | ## openLayerImage 47 | 48 | [Show source in __init__.py:35](../../../../layeredimage/io/__init__.py#L35) 49 | 50 | Open a layer image file into a layer image object. 51 | 52 | #### Arguments 53 | 54 | ---- 55 | - `file` *str* - path/ filename 56 | 57 | #### Raises 58 | 59 | ------ 60 | - `FileExistsError` - If the layered image does not exist 61 | - `ValueError` - If the extention is not recognised 62 | 63 | #### Returns 64 | 65 | ------- 66 | - `LayeredImage` - a layered image object 67 | 68 | #### Signature 69 | 70 | ```python 71 | def openLayerImage(file: str | Path) -> LayeredImage: ... 72 | ``` 73 | 74 | #### See also 75 | 76 | - [LayeredImage](../layeredimage.md#layeredimage) 77 | 78 | 79 | 80 | ## saveLayerImage 81 | 82 | [Show source in __init__.py:76](../../../../layeredimage/io/__init__.py#L76) 83 | 84 | Save a layered image to a file. 85 | 86 | #### Arguments 87 | 88 | ---- 89 | - `fileName` *str* - path/ filename 90 | - `layeredImage` *LayeredImage* - the layered image to save 91 | 92 | #### Raises 93 | 94 | ------ 95 | - `ValueError` - If the extention is not recognised 96 | 97 | #### Returns 98 | 99 | ------- 100 | None 101 | 102 | #### Signature 103 | 104 | ```python 105 | def saveLayerImage(fileName: str | Path, layeredImage: LayeredImage) -> None: ... 106 | ``` 107 | 108 | #### See also 109 | 110 | - [LayeredImage](../layeredimage.md#layeredimage) 111 | 112 | 113 | 114 | ## Modules 115 | 116 | - [Common](./common.md) 117 | - [Gif](./gif.md) 118 | - [Layered](./layered.md) 119 | - [Lsr](./lsr.md) 120 | - [Ora](./ora.md) 121 | - [Pdn](./pdn.md) 122 | - [Psd](./psd.md) 123 | - [Tiff](./tiff.md) 124 | - [Webp](./webp.md) 125 | - [Xcf](./xcf.md) -------------------------------------------------------------------------------- /documentation/reference/layeredimage/io/layered.md: -------------------------------------------------------------------------------- 1 | # Layered 2 | 3 | [Layeredimage Index](../../README.md#layeredimage-index) / [Layeredimage](../index.md#layeredimage) / [Io](./index.md#io) / Layered 4 | 5 | > Auto-generated documentation for [layeredimage.io.layered](../../../../layeredimage/io/layered.py) module. 6 | 7 | - [Layered](#layered) 8 | - [_saveLayer_LAYERED](#_savelayer_layered) 9 | - [grabLayer_LAYERED](#grablayer_layered) 10 | - [openLayer_LAYERED](#openlayer_layered) 11 | - [openLayer_LAYEREDC](#openlayer_layeredc) 12 | - [saveLayer_LAYERED](#savelayer_layered) 13 | - [saveLayer_LAYEREDC](#savelayer_layeredc) 14 | - [writeImage_LAYERED](#writeimage_layered) 15 | 16 | ## _saveLayer_LAYERED 17 | 18 | [Show source in layered.py:104](../../../../layeredimage/io/layered.py#L104) 19 | 20 | Save a layered image as .layered. 21 | 22 | #### Signature 23 | 24 | ```python 25 | def _saveLayer_LAYERED( 26 | fileName: str, layeredImage: LayeredImage, compressed: bool = False 27 | ) -> None: ... 28 | ``` 29 | 30 | #### See also 31 | 32 | - [LayeredImage](../layeredimage.md#layeredimage) 33 | 34 | 35 | 36 | ## grabLayer_LAYERED 37 | 38 | [Show source in layered.py:82](../../../../layeredimage/io/layered.py#L82) 39 | 40 | Grab an image from .layered. 41 | 42 | #### Signature 43 | 44 | ```python 45 | def grabLayer_LAYERED( 46 | zipFile: ZipFile, layer: dict[str, Any], blendLookup: dict[str, Any] 47 | ) -> Layer: ... 48 | ``` 49 | 50 | #### See also 51 | 52 | - [Layer](../layergroup.md#layer) 53 | 54 | 55 | 56 | ## openLayer_LAYERED 57 | 58 | [Show source in layered.py:20](../../../../layeredimage/io/layered.py#L20) 59 | 60 | Open a .layered file into a layered image. 61 | 62 | #### Signature 63 | 64 | ```python 65 | def openLayer_LAYERED(file: str) -> LayeredImage: ... 66 | ``` 67 | 68 | #### See also 69 | 70 | - [LayeredImage](../layeredimage.md#layeredimage) 71 | 72 | 73 | 74 | ## openLayer_LAYEREDC 75 | 76 | [Show source in layered.py:141](../../../../layeredimage/io/layered.py#L141) 77 | 78 | Open a .layeredc file into a layered image. 79 | 80 | #### Signature 81 | 82 | ```python 83 | def openLayer_LAYEREDC(file: str) -> LayeredImage: ... 84 | ``` 85 | 86 | #### See also 87 | 88 | - [LayeredImage](../layeredimage.md#layeredimage) 89 | 90 | 91 | 92 | ## saveLayer_LAYERED 93 | 94 | [Show source in layered.py:99](../../../../layeredimage/io/layered.py#L99) 95 | 96 | Save a layered image as .layered. 97 | 98 | #### Signature 99 | 100 | ```python 101 | def saveLayer_LAYERED(fileName: str, layeredImage: LayeredImage) -> None: ... 102 | ``` 103 | 104 | #### See also 105 | 106 | - [LayeredImage](../layeredimage.md#layeredimage) 107 | 108 | 109 | 110 | ## saveLayer_LAYEREDC 111 | 112 | [Show source in layered.py:146](../../../../layeredimage/io/layered.py#L146) 113 | 114 | Save a layeredc image as .layered. 115 | 116 | #### Signature 117 | 118 | ```python 119 | def saveLayer_LAYEREDC(fileName: str, layeredImage: LayeredImage) -> None: ... 120 | ``` 121 | 122 | #### See also 123 | 124 | - [LayeredImage](../layeredimage.md#layeredimage) 125 | 126 | 127 | 128 | ## writeImage_LAYERED 129 | 130 | [Show source in layered.py:126](../../../../layeredimage/io/layered.py#L126) 131 | 132 | Write an image to the archive. 133 | 134 | #### Signature 135 | 136 | ```python 137 | def writeImage_LAYERED( 138 | image: Image.Image, zipFile: ZipFile, path: str, compressed: bool = False 139 | ) -> None: ... 140 | ``` -------------------------------------------------------------------------------- /documentation/reference/layeredimage/io/lsr.md: -------------------------------------------------------------------------------- 1 | # Lsr 2 | 3 | [Layeredimage Index](../../README.md#layeredimage-index) / [Layeredimage](../index.md#layeredimage) / [Io](./index.md#io) / Lsr 4 | 5 | > Auto-generated documentation for [layeredimage.io.lsr](../../../../layeredimage/io/lsr.py) module. 6 | 7 | - [Lsr](#lsr) 8 | - [openLayer_LSR](#openlayer_lsr) 9 | - [saveLayer_LSR](#savelayer_lsr) 10 | 11 | ## openLayer_LSR 12 | 13 | [Show source in lsr.py:13](../../../../layeredimage/io/lsr.py#L13) 14 | 15 | Open a .lsr file into a layered image. 16 | 17 | #### Signature 18 | 19 | ```python 20 | def openLayer_LSR(file: str) -> LayeredImage: ... 21 | ``` 22 | 23 | #### See also 24 | 25 | - [LayeredImage](../layeredimage.md#layeredimage) 26 | 27 | 28 | 29 | ## saveLayer_LSR 30 | 31 | [Show source in lsr.py:38](../../../../layeredimage/io/lsr.py#L38) 32 | 33 | Save a layered image as .lsr. 34 | 35 | #### Signature 36 | 37 | ```python 38 | def saveLayer_LSR(fileName: str, layeredImage: LayeredImage) -> None: ... 39 | ``` 40 | 41 | #### See also 42 | 43 | - [LayeredImage](../layeredimage.md#layeredimage) -------------------------------------------------------------------------------- /documentation/reference/layeredimage/io/ora.md: -------------------------------------------------------------------------------- 1 | # Ora 2 | 3 | [Layeredimage Index](../../README.md#layeredimage-index) / [Layeredimage](../index.md#layeredimage) / [Io](./index.md#io) / Ora 4 | 5 | > Auto-generated documentation for [layeredimage.io.ora](../../../../layeredimage/io/ora.py) module. 6 | 7 | - [Ora](#ora) 8 | - [addLayer_ORA](#addlayer_ora) 9 | - [openLayer_ORA](#openlayer_ora) 10 | - [saveLayer_ORA](#savelayer_ora) 11 | 12 | ## addLayer_ORA 13 | 14 | [Show source in ora.py:129](../../../../layeredimage/io/ora.py#L129) 15 | 16 | Update the project with a shiny new layer. 17 | 18 | #### Signature 19 | 20 | ```python 21 | def addLayer_ORA(project: Any, layer: Any, blendLookup: dict[BlendType, str]) -> Any: ... 22 | ``` 23 | 24 | 25 | 26 | ## openLayer_ORA 27 | 28 | [Show source in ora.py:15](../../../../layeredimage/io/ora.py#L15) 29 | 30 | Open an .ora file into a layered image. 31 | 32 | #### Signature 33 | 34 | ```python 35 | def openLayer_ORA(file: str) -> LayeredImage: ... 36 | ``` 37 | 38 | #### See also 39 | 40 | - [LayeredImage](../layeredimage.md#layeredimage) 41 | 42 | 43 | 44 | ## saveLayer_ORA 45 | 46 | [Show source in ora.py:85](../../../../layeredimage/io/ora.py#L85) 47 | 48 | Save a layered image as .ora. 49 | 50 | #### Signature 51 | 52 | ```python 53 | def saveLayer_ORA(fileName: str, layeredImage: LayeredImage) -> None: ... 54 | ``` 55 | 56 | #### See also 57 | 58 | - [LayeredImage](../layeredimage.md#layeredimage) -------------------------------------------------------------------------------- /documentation/reference/layeredimage/io/pdn.md: -------------------------------------------------------------------------------- 1 | # Pdn 2 | 3 | [Layeredimage Index](../../README.md#layeredimage-index) / [Layeredimage](../index.md#layeredimage) / [Io](./index.md#io) / Pdn 4 | 5 | > Auto-generated documentation for [layeredimage.io.pdn](../../../../layeredimage/io/pdn.py) module. 6 | 7 | - [Pdn](#pdn) 8 | - [openLayer_PDN](#openlayer_pdn) 9 | - [saveLayer_PDN](#savelayer_pdn) 10 | 11 | ## openLayer_PDN 12 | 13 | [Show source in pdn.py:15](../../../../layeredimage/io/pdn.py#L15) 14 | 15 | Open a .pdn file into a layered image. 16 | 17 | #### Signature 18 | 19 | ```python 20 | def openLayer_PDN(file: str) -> LayeredImage: ... 21 | ``` 22 | 23 | #### See also 24 | 25 | - [LayeredImage](../layeredimage.md#layeredimage) 26 | 27 | 28 | 29 | ## saveLayer_PDN 30 | 31 | [Show source in pdn.py:54](../../../../layeredimage/io/pdn.py#L54) 32 | 33 | Save a layered image as .pdn. 34 | 35 | #### Signature 36 | 37 | ```python 38 | def saveLayer_PDN(fileName: str, layeredImage: LayeredImage) -> None: ... 39 | ``` 40 | 41 | #### See also 42 | 43 | - [LayeredImage](../layeredimage.md#layeredimage) -------------------------------------------------------------------------------- /documentation/reference/layeredimage/io/psd.md: -------------------------------------------------------------------------------- 1 | # Psd 2 | 3 | [Layeredimage Index](../../README.md#layeredimage-index) / [Layeredimage](../index.md#layeredimage) / [Io](./index.md#io) / Psd 4 | 5 | > Auto-generated documentation for [layeredimage.io.psd](../../../../layeredimage/io/psd.py) module. 6 | 7 | - [Psd](#psd) 8 | - [openLayer_PSD](#openlayer_psd) 9 | - [saveLayer_PSD](#savelayer_psd) 10 | 11 | ## openLayer_PSD 12 | 13 | [Show source in psd.py:14](../../../../layeredimage/io/psd.py#L14) 14 | 15 | Open a .psd file into a layered image. 16 | 17 | #### Signature 18 | 19 | ```python 20 | def openLayer_PSD(file: str) -> LayeredImage: ... 21 | ``` 22 | 23 | #### See also 24 | 25 | - [LayeredImage](../layeredimage.md#layeredimage) 26 | 27 | 28 | 29 | ## saveLayer_PSD 30 | 31 | [Show source in psd.py:84](../../../../layeredimage/io/psd.py#L84) 32 | 33 | Save a layered image as .psd. 34 | 35 | #### Signature 36 | 37 | ```python 38 | def saveLayer_PSD(fileName: str, layeredImage: LayeredImage) -> None: ... 39 | ``` 40 | 41 | #### See also 42 | 43 | - [LayeredImage](../layeredimage.md#layeredimage) -------------------------------------------------------------------------------- /documentation/reference/layeredimage/io/tiff.md: -------------------------------------------------------------------------------- 1 | # Tiff 2 | 3 | [Layeredimage Index](../../README.md#layeredimage-index) / [Layeredimage](../index.md#layeredimage) / [Io](./index.md#io) / Tiff 4 | 5 | > Auto-generated documentation for [layeredimage.io.tiff](../../../../layeredimage/io/tiff.py) module. 6 | 7 | - [Tiff](#tiff) 8 | - [openLayer_TIFF](#openlayer_tiff) 9 | - [saveLayer_TIFF](#savelayer_tiff) 10 | 11 | ## openLayer_TIFF 12 | 13 | [Show source in tiff.py:13](../../../../layeredimage/io/tiff.py#L13) 14 | 15 | Open a .tiff or a .tif file into a layered image. 16 | 17 | #### Signature 18 | 19 | ```python 20 | def openLayer_TIFF(file: str) -> LayeredImage: ... 21 | ``` 22 | 23 | #### See also 24 | 25 | - [LayeredImage](../layeredimage.md#layeredimage) 26 | 27 | 28 | 29 | ## saveLayer_TIFF 30 | 31 | [Show source in tiff.py:52](../../../../layeredimage/io/tiff.py#L52) 32 | 33 | Save a layered image as .tiff or .tif. 34 | 35 | #### Signature 36 | 37 | ```python 38 | def saveLayer_TIFF(fileName: str, layeredImage: LayeredImage) -> None: ... 39 | ``` 40 | 41 | #### See also 42 | 43 | - [LayeredImage](../layeredimage.md#layeredimage) -------------------------------------------------------------------------------- /documentation/reference/layeredimage/io/webp.md: -------------------------------------------------------------------------------- 1 | # Webp 2 | 3 | [Layeredimage Index](../../README.md#layeredimage-index) / [Layeredimage](../index.md#layeredimage) / [Io](./index.md#io) / Webp 4 | 5 | > Auto-generated documentation for [layeredimage.io.webp](../../../../layeredimage/io/webp.py) module. 6 | 7 | - [Webp](#webp) 8 | - [openLayer_WEBP](#openlayer_webp) 9 | - [saveLayer_WEBP](#savelayer_webp) 10 | 11 | ## openLayer_WEBP 12 | 13 | [Show source in webp.py:13](../../../../layeredimage/io/webp.py#L13) 14 | 15 | Open a .webp file into a layered image. 16 | 17 | #### Signature 18 | 19 | ```python 20 | def openLayer_WEBP(file: str) -> LayeredImage: ... 21 | ``` 22 | 23 | #### See also 24 | 25 | - [LayeredImage](../layeredimage.md#layeredimage) 26 | 27 | 28 | 29 | ## saveLayer_WEBP 30 | 31 | [Show source in webp.py:27](../../../../layeredimage/io/webp.py#L27) 32 | 33 | Save a layered image as .webp. 34 | 35 | #### Signature 36 | 37 | ```python 38 | def saveLayer_WEBP(fileName: str, layeredImage: LayeredImage) -> None: ... 39 | ``` 40 | 41 | #### See also 42 | 43 | - [LayeredImage](../layeredimage.md#layeredimage) -------------------------------------------------------------------------------- /documentation/reference/layeredimage/io/xcf.md: -------------------------------------------------------------------------------- 1 | # Xcf 2 | 3 | [Layeredimage Index](../../README.md#layeredimage-index) / [Layeredimage](../index.md#layeredimage) / [Io](./index.md#io) / Xcf 4 | 5 | > Auto-generated documentation for [layeredimage.io.xcf](../../../../layeredimage/io/xcf.py) module. 6 | 7 | - [Xcf](#xcf) 8 | - [openLayer_XCF](#openlayer_xcf) 9 | - [saveLayer_XCF](#savelayer_xcf) 10 | 11 | ## openLayer_XCF 12 | 13 | [Show source in xcf.py:14](../../../../layeredimage/io/xcf.py#L14) 14 | 15 | Open an .xcf file into a layered image. 16 | 17 | #### Signature 18 | 19 | ```python 20 | def openLayer_XCF(file: str) -> LayeredImage: ... 21 | ``` 22 | 23 | #### See also 24 | 25 | - [LayeredImage](../layeredimage.md#layeredimage) 26 | 27 | 28 | 29 | ## saveLayer_XCF 30 | 31 | [Show source in xcf.py:134](../../../../layeredimage/io/xcf.py#L134) 32 | 33 | Save a layered image as .xcf. 34 | 35 | #### Signature 36 | 37 | ```python 38 | def saveLayer_XCF(fileName: str, layeredImage: LayeredImage) -> None: ... 39 | ``` 40 | 41 | #### See also 42 | 43 | - [LayeredImage](../layeredimage.md#layeredimage) -------------------------------------------------------------------------------- /documentation/reference/layeredimage/layeredimage.md: -------------------------------------------------------------------------------- 1 | # LayeredImage 2 | 3 | [Layeredimage Index](../README.md#layeredimage-index) / [Layeredimage](./index.md#layeredimage) / LayeredImage 4 | 5 | > Auto-generated documentation for [layeredimage.layeredimage](../../../layeredimage/layeredimage.py) module. 6 | 7 | - [LayeredImage](#layeredimage) 8 | - [LayeredImage](#layeredimage-1) 9 | - [LayeredImage().__repr__](#layeredimage()__repr__) 10 | - [LayeredImage().__str__](#layeredimage()__str__) 11 | - [LayeredImage().addLayerOrGroup](#layeredimage()addlayerorgroup) 12 | - [LayeredImage().extractGroups](#layeredimage()extractgroups) 13 | - [LayeredImage().extractLayers](#layeredimage()extractlayers) 14 | - [LayeredImage().getFlattenLayers](#layeredimage()getflattenlayers) 15 | - [LayeredImage().getLayerOrGroup](#layeredimage()getlayerorgroup) 16 | - [LayeredImage().insertLayerOrGroup](#layeredimage()insertlayerorgroup) 17 | - [LayeredImage().json](#layeredimage()json) 18 | - [LayeredImage().removeLayerOrGroup](#layeredimage()removelayerorgroup) 19 | - [LayeredImage().updateGroups](#layeredimage()updategroups) 20 | - [LayeredImage().updateLayers](#layeredimage()updatelayers) 21 | - [render](#render) 22 | 23 | ## LayeredImage 24 | 25 | [Show source in layeredimage.py:14](../../../layeredimage/layeredimage.py#L14) 26 | 27 | A representation of a layered image such as an ora. 28 | 29 | #### Signature 30 | 31 | ```python 32 | class LayeredImage: 33 | def __init__( 34 | self, 35 | layersAndGroups: list[Layer | Group], 36 | dimensions: tuple[int, int] | None = None, 37 | **kwargs: dict[str, Any] 38 | ) -> None: ... 39 | ``` 40 | 41 | ### LayeredImage().__repr__ 42 | 43 | [Show source in layeredimage.py:49](../../../layeredimage/layeredimage.py#L49) 44 | 45 | Get the string representation. 46 | 47 | #### Signature 48 | 49 | ```python 50 | def __repr__(self) -> str: ... 51 | ``` 52 | 53 | ### LayeredImage().__str__ 54 | 55 | [Show source in layeredimage.py:53](../../../layeredimage/layeredimage.py#L53) 56 | 57 | Get the string representation. 58 | 59 | #### Signature 60 | 61 | ```python 62 | def __str__(self) -> str: ... 63 | ``` 64 | 65 | ### LayeredImage().addLayerOrGroup 66 | 67 | [Show source in layeredimage.py:70](../../../layeredimage/layeredimage.py#L70) 68 | 69 | Add a LayerOrGroup. 70 | 71 | #### Signature 72 | 73 | ```python 74 | def addLayerOrGroup(self, layerOrGroup: Layer | Group) -> None: ... 75 | ``` 76 | 77 | ### LayeredImage().extractGroups 78 | 79 | [Show source in layeredimage.py:127](../../../layeredimage/layeredimage.py#L127) 80 | 81 | Extract the groups from the image. 82 | 83 | #### Signature 84 | 85 | ```python 86 | def extractGroups(self) -> list[Group]: ... 87 | ``` 88 | 89 | #### See also 90 | 91 | - [Group](./layergroup.md#group) 92 | 93 | ### LayeredImage().extractLayers 94 | 95 | [Show source in layeredimage.py:95](../../../layeredimage/layeredimage.py#L95) 96 | 97 | Extract the layers from the image. 98 | 99 | #### Signature 100 | 101 | ```python 102 | def extractLayers(self) -> list[Layer]: ... 103 | ``` 104 | 105 | #### See also 106 | 107 | - [Layer](./layergroup.md#layer) 108 | 109 | ### LayeredImage().getFlattenLayers 110 | 111 | [Show source in layeredimage.py:83](../../../layeredimage/layeredimage.py#L83) 112 | 113 | Return an image for all flattened layers. 114 | 115 | #### Signature 116 | 117 | ```python 118 | def getFlattenLayers(self) -> Image.Image: ... 119 | ``` 120 | 121 | ### LayeredImage().getLayerOrGroup 122 | 123 | [Show source in layeredimage.py:66](../../../layeredimage/layeredimage.py#L66) 124 | 125 | Get a LayerOrGroup. 126 | 127 | #### Signature 128 | 129 | ```python 130 | def getLayerOrGroup(self, index: int) -> Layer | Group: ... 131 | ``` 132 | 133 | ### LayeredImage().insertLayerOrGroup 134 | 135 | [Show source in layeredimage.py:74](../../../layeredimage/layeredimage.py#L74) 136 | 137 | Insert a LayerOrGroup at a specific index. 138 | 139 | #### Signature 140 | 141 | ```python 142 | def insertLayerOrGroup(self, layerOrGroup: Layer | Group, index: int) -> None: ... 143 | ``` 144 | 145 | ### LayeredImage().json 146 | 147 | [Show source in layeredimage.py:60](../../../layeredimage/layeredimage.py#L60) 148 | 149 | Get the object as a dict. 150 | 151 | #### Signature 152 | 153 | ```python 154 | def json(self) -> dict[str, Any]: ... 155 | ``` 156 | 157 | ### LayeredImage().removeLayerOrGroup 158 | 159 | [Show source in layeredimage.py:78](../../../layeredimage/layeredimage.py#L78) 160 | 161 | Remove a LayerOrGroup at a specific index. 162 | 163 | #### Signature 164 | 165 | ```python 166 | def removeLayerOrGroup(self, index: int) -> None: ... 167 | ``` 168 | 169 | ### LayeredImage().updateGroups 170 | 171 | [Show source in layeredimage.py:135](../../../layeredimage/layeredimage.py#L135) 172 | 173 | Update the groups from the image. 174 | 175 | #### Signature 176 | 177 | ```python 178 | def updateGroups(self) -> None: ... 179 | ``` 180 | 181 | ### LayeredImage().updateLayers 182 | 183 | [Show source in layeredimage.py:123](../../../layeredimage/layeredimage.py#L123) 184 | 185 | Update the layers from the image. 186 | 187 | #### Signature 188 | 189 | ```python 190 | def updateLayers(self) -> None: ... 191 | ``` 192 | 193 | 194 | 195 | ## render 196 | 197 | [Show source in layeredimage.py:140](../../../layeredimage/layeredimage.py#L140) 198 | 199 | Flatten a layer or group on to an image of what has already been flattened. 200 | 201 | #### Arguments 202 | 203 | ---- 204 | layerOrGroup (Layer, Group): A layer or a group of layers 205 | - `project_image` *np.ndarray, optional* - the image of what has already 206 | been flattened. 207 | 208 | #### Returns 209 | 210 | ------- 211 | - `np.ndarray` - Flattened image 212 | 213 | #### Signature 214 | 215 | ```python 216 | def render(layerOrGroup: Layer | Group, project_image: np.ndarray) -> np.ndarray: ... 217 | ``` -------------------------------------------------------------------------------- /documentation/reference/layeredimage/layergroup.md: -------------------------------------------------------------------------------- 1 | # LayerGroup 2 | 3 | [Layeredimage Index](../README.md#layeredimage-index) / [Layeredimage](./index.md#layeredimage) / LayerGroup 4 | 5 | > Auto-generated documentation for [layeredimage.layergroup](../../../layeredimage/layergroup.py) module. 6 | 7 | - [LayerGroup](#layergroup) 8 | - [Group](#group) 9 | - [Group().json](#group()json) 10 | - [Layer](#layer) 11 | - [Layer().json](#layer()json) 12 | - [LayerGroup](#layergroup-1) 13 | - [LayerGroup().__repr__](#layergroup()__repr__) 14 | - [LayerGroup().__str__](#layergroup()__str__) 15 | - [LayerGroup().json](#layergroup()json) 16 | 17 | ## Group 18 | 19 | [Show source in layergroup.py:124](../../../layeredimage/layergroup.py#L124) 20 | 21 | A representation of an image group. 22 | 23 | #### Signature 24 | 25 | ```python 26 | class Group(LayerGroup): 27 | def __init__( 28 | self, 29 | name: str, 30 | layers: list[Layer], 31 | dimensions: tuple[int, int] | None = None, 32 | offsets: tuple[int, int] = (0, 0), 33 | opacity: float = 1.0, 34 | visible: bool = True, 35 | blendmode: BlendType = BlendType.NORMAL, 36 | ) -> None: ... 37 | ``` 38 | 39 | #### See also 40 | 41 | - [LayerGroup](#layergroup) 42 | - [Layer](#layer) 43 | 44 | ### Group().json 45 | 46 | [Show source in layergroup.py:172](../../../layeredimage/layergroup.py#L172) 47 | 48 | Get the object as a dict. 49 | 50 | #### Signature 51 | 52 | ```python 53 | def json(self) -> dict[str, Any]: ... 54 | ``` 55 | 56 | 57 | 58 | ## Layer 59 | 60 | [Show source in layergroup.py:71](../../../layeredimage/layergroup.py#L71) 61 | 62 | A representation of an image layer. 63 | 64 | #### Signature 65 | 66 | ```python 67 | class Layer(LayerGroup): 68 | def __init__( 69 | self, 70 | name: str, 71 | image: Image.Image, 72 | dimensions: tuple[int, int] | None = None, 73 | offsets: tuple[int, int] = (0, 0), 74 | opacity: float = 1.0, 75 | visible: bool = True, 76 | blendmode: BlendType = BlendType.NORMAL, 77 | ) -> None: ... 78 | ``` 79 | 80 | #### See also 81 | 82 | - [LayerGroup](#layergroup) 83 | 84 | ### Layer().json 85 | 86 | [Show source in layergroup.py:111](../../../layeredimage/layergroup.py#L111) 87 | 88 | Get the object as a dict. 89 | 90 | #### Signature 91 | 92 | ```python 93 | def json(self) -> dict[str, Any]: ... 94 | ``` 95 | 96 | 97 | 98 | ## LayerGroup 99 | 100 | [Show source in layergroup.py:11](../../../layeredimage/layergroup.py#L11) 101 | 102 | A representation of an image layer or group. 103 | 104 | #### Signature 105 | 106 | ```python 107 | class LayerGroup: 108 | def __init__( 109 | self, 110 | name: str, 111 | dimensions: tuple[int, int], 112 | offsets: tuple[int, int] = (0, 0), 113 | opacity: float = 1.0, 114 | visible: bool = True, 115 | blendmode: BlendType = BlendType.NORMAL, 116 | **kwargs: dict[str, Any] 117 | ) -> None: ... 118 | ``` 119 | 120 | ### LayerGroup().__repr__ 121 | 122 | [Show source in layergroup.py:51](../../../layeredimage/layergroup.py#L51) 123 | 124 | Get the string representation. 125 | 126 | #### Signature 127 | 128 | ```python 129 | def __repr__(self) -> str: ... 130 | ``` 131 | 132 | ### LayerGroup().__str__ 133 | 134 | [Show source in layergroup.py:55](../../../layeredimage/layergroup.py#L55) 135 | 136 | Get the string representation. 137 | 138 | #### Signature 139 | 140 | ```python 141 | def __str__(self) -> str: ... 142 | ``` 143 | 144 | ### LayerGroup().json 145 | 146 | [Show source in layergroup.py:59](../../../layeredimage/layergroup.py#L59) 147 | 148 | Get the object as a dict. 149 | 150 | #### Signature 151 | 152 | ```python 153 | def json(self) -> dict[str, Any]: ... 154 | ``` -------------------------------------------------------------------------------- /layeredimage/__init__.py: -------------------------------------------------------------------------------- 1 | """Use this module to read, and write to a number of layered image formats.""" 2 | -------------------------------------------------------------------------------- /layeredimage/io/__init__.py: -------------------------------------------------------------------------------- 1 | """Do file io.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pathlib import Path 6 | 7 | from loguru import logger 8 | 9 | from layeredimage.io.gif import openLayer_GIF, saveLayer_GIF 10 | from layeredimage.io.layered import ( 11 | openLayer_LAYERED, 12 | openLayer_LAYEREDC, 13 | saveLayer_LAYERED, 14 | saveLayer_LAYEREDC, 15 | ) 16 | from layeredimage.io.lsr import openLayer_LSR, saveLayer_LSR 17 | from layeredimage.io.ora import openLayer_ORA, saveLayer_ORA 18 | from layeredimage.io.pdn import openLayer_PDN, saveLayer_PDN 19 | from layeredimage.io.psd import openLayer_PSD, saveLayer_PSD 20 | from layeredimage.io.tiff import openLayer_TIFF, saveLayer_TIFF 21 | from layeredimage.io.webp import openLayer_WEBP, saveLayer_WEBP 22 | from layeredimage.io.xcf import openLayer_XCF, saveLayer_XCF 23 | from layeredimage.layeredimage import LayeredImage 24 | 25 | 26 | def extNotRecognised(fileName: str) -> None: 27 | """Output the file extension not recognised error.""" 28 | exts = ["ora", "psd", "xcf", "pdn", "tif", "tiff", "webp", "gif", "lsr", "layered", "layeredc"] 29 | logger.error( 30 | ".File extension is not recognised for file! Must be one of " + ', "'.join(exts) + '"', 31 | extra={"fileName": fileName}, 32 | ) 33 | 34 | 35 | def openLayerImage(file: str | Path) -> LayeredImage: 36 | """Open a layer image file into a layer image object. 37 | 38 | Args: 39 | ---- 40 | file (str): path/ filename 41 | 42 | Raises: 43 | ------ 44 | FileExistsError: If the layered image does not exist 45 | ValueError: If the extention is not recognised 46 | 47 | Returns: 48 | ------- 49 | LayeredImage: a layered image object 50 | 51 | """ 52 | functionMap = { 53 | ".ora": openLayer_ORA, 54 | ".psd": openLayer_PSD, 55 | ".xcf": openLayer_XCF, 56 | ".pdn": openLayer_PDN, 57 | ".tif": openLayer_TIFF, 58 | ".tiff": openLayer_TIFF, 59 | ".webp": openLayer_WEBP, 60 | ".gif": openLayer_GIF, 61 | ".lsr": openLayer_LSR, 62 | ".layered": openLayer_LAYERED, 63 | ".layeredc": openLayer_LAYEREDC, 64 | } 65 | fp = Path(file) 66 | if not fp.is_file(): 67 | logger.error("File does not exist", extra={"file": file}) 68 | raise FileExistsError 69 | fileExt = fp.suffix.lower() 70 | if fileExt not in functionMap: 71 | extNotRecognised(fp.name) 72 | raise ValueError 73 | return functionMap[fileExt](fp.as_posix()) 74 | 75 | 76 | def saveLayerImage(fileName: str | Path, layeredImage: LayeredImage) -> None: 77 | """Save a layered image to a file. 78 | 79 | Args: 80 | ---- 81 | fileName (str): path/ filename 82 | layeredImage (LayeredImage): the layered image to save 83 | 84 | Raises: 85 | ------ 86 | ValueError: If the extention is not recognised 87 | 88 | Returns: 89 | ------- 90 | None 91 | 92 | """ 93 | functionMap = { 94 | ".ora": saveLayer_ORA, 95 | ".psd": saveLayer_PSD, 96 | ".xcf": saveLayer_XCF, 97 | ".pdn": saveLayer_PDN, 98 | ".tif": saveLayer_TIFF, 99 | ".tiff": saveLayer_TIFF, 100 | ".webp": saveLayer_WEBP, 101 | ".gif": saveLayer_GIF, 102 | ".lsr": saveLayer_LSR, 103 | ".layered": saveLayer_LAYERED, 104 | ".layeredc": saveLayer_LAYEREDC, 105 | } 106 | pth = Path(fileName) 107 | fileExt = pth.suffix.lower() 108 | if fileExt not in functionMap: 109 | extNotRecognised(pth.name) 110 | raise ValueError 111 | return functionMap[fileExt](pth.as_posix(), layeredImage) 112 | 113 | 114 | def exportFlatImage(fileName: str, layeredImage: LayeredImage) -> None: 115 | """Export the layered image to a unilayer image file.""" 116 | layeredImage.getFlattenLayers().save(fileName) 117 | -------------------------------------------------------------------------------- /layeredimage/io/common.py: -------------------------------------------------------------------------------- 1 | """Do file io - Common Operations for file readers/writers.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Any 6 | 7 | import numpy as np 8 | from blendmodes.blend import BlendType 9 | from loguru import logger 10 | from PIL import Image 11 | 12 | from layeredimage.layeredimage import LayeredImage 13 | 14 | 15 | def blendModeLookup( 16 | blendmode: Any, 17 | blendLookup: dict[Any, Any], 18 | default: BlendType | tuple[str, ...] = BlendType.NORMAL, 19 | ) -> BlendType: 20 | """Get the blendmode from a lookup table.""" 21 | if blendmode not in blendLookup: 22 | logger.warning("Blendmode is not currently supported!", extra={"blendmode": blendmode}) 23 | return default 24 | return blendLookup[blendmode] 25 | 26 | 27 | def expandLayersToCanvas(layeredImage: LayeredImage, imageFormat: str) -> list[Image.Image]: 28 | """Return layers and throw a warning if the image has groups.""" 29 | if len(layeredImage.extractGroups()) > 0: 30 | logger.warning( 31 | "This image format does not support groups so extracting layers", 32 | extra={"imageFormat": imageFormat}, 33 | ) 34 | return [ 35 | expandLayer( 36 | dimensions=layeredImage.dimensions, 37 | foreground=layer.image, 38 | opacity=layer.opacity, 39 | offsets=layer.offsets, 40 | ) 41 | for layer in layeredImage.extractLayers() 42 | ] 43 | 44 | 45 | def expandLayer( 46 | dimensions: tuple[int, int], 47 | foreground: np.ndarray | Image.Image, 48 | opacity: float = 1.0, 49 | offsets: tuple[int, int] = (0, 0), 50 | ) -> Image.Image: 51 | """ 52 | Args: 53 | ---- 54 | foreground (np.ndarray | Image.Image): The foreground layer (must be the same size as the background). 55 | opacity (float, optional): The opacity of the foreground image. Defaults to 1.0. 56 | offsets (Tuple[int, int], optional): Offsets for the foreground layer. Defaults to (0, 0). 57 | 58 | Returns: 59 | ------- 60 | Image.Image: The image. 61 | 62 | """ 63 | # Convert the Image.Image to a numpy array if required 64 | if isinstance(foreground, Image.Image): 65 | foreground = np.array(foreground.convert("RGBA")) 66 | 67 | # do any offset shifting first 68 | if offsets[0] > 0: 69 | foreground = np.hstack( 70 | (np.zeros((foreground.shape[0], offsets[0], 4), dtype=np.float64), foreground) 71 | ) 72 | elif offsets[0] < 0: 73 | if offsets[0] > -1 * foreground.shape[1]: 74 | foreground = foreground[:, -1 * offsets[0] :, :] 75 | else: 76 | # offset offscreen completely, there is nothing left 77 | return Image.fromarray(np.zeros(dimensions, dtype=np.uint8)) 78 | if offsets[1] > 0: 79 | foreground = np.vstack( 80 | (np.zeros((offsets[1], foreground.shape[1], 4), dtype=np.float64), foreground) 81 | ) 82 | elif offsets[1] < 0: 83 | if offsets[1] > -1 * foreground.shape[0]: 84 | foreground = foreground[-1 * offsets[1] :, :, :] 85 | else: 86 | # offset offscreen completely, there is nothing left 87 | return Image.fromarray(np.zeros(dimensions, dtype=np.uint8)) 88 | 89 | # resize array to fill small images with zeros 90 | if foreground.shape[0] < dimensions[0]: 91 | foreground = np.vstack( 92 | ( 93 | foreground, 94 | np.zeros( 95 | (dimensions[0] - foreground.shape[0], foreground.shape[1], 4), 96 | dtype=np.float64, 97 | ), 98 | ) 99 | ) 100 | if foreground.shape[1] < dimensions[1]: 101 | foreground = np.hstack( 102 | ( 103 | foreground, 104 | np.zeros( 105 | (foreground.shape[0], dimensions[1] - foreground.shape[1], 4), 106 | dtype=np.float64, 107 | ), 108 | ) 109 | ) 110 | 111 | # crop the source if the backdrop is smaller 112 | foreground = foreground[: dimensions[0], : dimensions[1], :] 113 | 114 | upper_norm = foreground 115 | 116 | upper_alpha = upper_norm[:, :, 3] * opacity 117 | upper_rgb = upper_norm[:, :, :3] 118 | 119 | arr = np.nan_to_num(np.dstack((upper_rgb, upper_alpha)), copy=False) 120 | return Image.fromarray(np.uint8(np.around(arr, 0))) 121 | -------------------------------------------------------------------------------- /layeredimage/io/gif.py: -------------------------------------------------------------------------------- 1 | """Do file io - GIF.""" 2 | 3 | from __future__ import annotations 4 | 5 | from PIL import Image 6 | 7 | from layeredimage.io.common import expandLayersToCanvas 8 | from layeredimage.layeredimage import LayeredImage 9 | from layeredimage.layergroup import Layer 10 | 11 | 12 | ## GIF ## 13 | def openLayer_GIF(file: str) -> LayeredImage: 14 | """Open a .gif file into a layered image.""" 15 | project = Image.open(file) 16 | projectSize = project.size 17 | layers = [] 18 | for index in range(project.n_frames): 19 | project.seek(index) 20 | layers.append( 21 | Layer( 22 | name=f"Frame {len(layers) + 1} ({project.info['duration']}ms)", 23 | image=project.copy(), 24 | dimensions=projectSize, 25 | ) 26 | ) 27 | project.close() 28 | return LayeredImage(layers, projectSize) 29 | 30 | 31 | def saveLayer_GIF(fileName: str, layeredImage: LayeredImage) -> None: 32 | """Save a layered image as .gif.""" 33 | layers = expandLayersToCanvas(layeredImage, "GIF") 34 | layers[0].save( 35 | fileName, 36 | duration=200, 37 | append_images=layers[1:], 38 | version="GIF89a", 39 | disposal=2, 40 | save_all=True, 41 | loop=0, 42 | transparency=0, 43 | ) 44 | -------------------------------------------------------------------------------- /layeredimage/io/layered.py: -------------------------------------------------------------------------------- 1 | """Do file io - LAYERED(C).""" 2 | 3 | from __future__ import annotations 4 | 5 | import io 6 | import json 7 | import zipfile 8 | from typing import Any 9 | from zipfile import ZipFile 10 | 11 | from blendmodes.blend import BlendType 12 | from PIL import Image 13 | 14 | from layeredimage.io.common import blendModeLookup 15 | from layeredimage.layeredimage import LayeredImage 16 | from layeredimage.layergroup import Group, Layer 17 | 18 | 19 | ## LAYERED ## 20 | def openLayer_LAYERED(file: str) -> LayeredImage: 21 | """Open a .layered file into a layered image.""" 22 | blendLookup = { 23 | "NORMAL": BlendType.NORMAL, 24 | "MULTIPLY": BlendType.MULTIPLY, 25 | "ADDITIVE": BlendType.ADDITIVE, 26 | "COLOURBURN": BlendType.COLOURBURN, 27 | "COLOURDODGE": BlendType.COLOURDODGE, 28 | "REFLECT": BlendType.REFLECT, 29 | "GLOW": BlendType.GLOW, 30 | "OVERLAY": BlendType.OVERLAY, 31 | "DIFFERENCE": BlendType.DIFFERENCE, 32 | "NEGATION": BlendType.NEGATION, 33 | "LIGHTEN": BlendType.LIGHTEN, 34 | "DARKEN": BlendType.DARKEN, 35 | "SCREEN": BlendType.SCREEN, 36 | "XOR": BlendType.XOR, 37 | "SOFTLIGHT": BlendType.SOFTLIGHT, 38 | "HARDLIGHT": BlendType.HARDLIGHT, 39 | "GRAINEXTRACT": BlendType.GRAINEXTRACT, 40 | "GRAINMERGE": BlendType.GRAINMERGE, 41 | "DIVIDE": BlendType.DIVIDE, 42 | "HUE": BlendType.HUE, 43 | "SATURATION": BlendType.SATURATION, 44 | "COLOUR": BlendType.COLOUR, 45 | "LUMINOSITY": BlendType.LUMINOSITY, 46 | "PINLIGHT": BlendType.PINLIGHT, 47 | "VIVIDLIGHT": BlendType.VIVIDLIGHT, 48 | "EXCLUSION": BlendType.EXCLUSION, 49 | "DESTIN": BlendType.DESTIN, 50 | "DESTOUT": BlendType.DESTOUT, 51 | "DESTATOP": BlendType.DESTATOP, 52 | "SRCATOP": BlendType.SRCATOP, 53 | } 54 | layersAndGroups = [] 55 | with zipfile.ZipFile(file, "r") as layered: 56 | with layered.open("stack.json") as stackJson: 57 | stack = json.load(stackJson) 58 | # Iterate through the layers and groups 59 | for layerOrGroup in stack["layersAndGroups"]: 60 | if layerOrGroup["type"] == "LAYER": 61 | layersAndGroups.append(grabLayer_LAYERED(layered, layerOrGroup, blendLookup)) 62 | else: 63 | # If its a group grab the layers 64 | layers = [ 65 | grabLayer_LAYERED(layered, layer, blendLookup) 66 | for layer in layerOrGroup["layers"] 67 | ] 68 | layersAndGroups.append( 69 | Group( 70 | name=layerOrGroup["name"], 71 | layers=layers, 72 | dimensions=layerOrGroup["dimensions"], 73 | offsets=layerOrGroup["offsets"], 74 | opacity=layerOrGroup["opacity"], 75 | visible=layerOrGroup["visible"], 76 | blendmode=blendModeLookup(layerOrGroup["blendmode"], blendLookup), 77 | ) 78 | ) 79 | return LayeredImage(layersAndGroups, stack["dimensions"]) 80 | 81 | 82 | def grabLayer_LAYERED( 83 | zipFile: ZipFile, layer: dict[str, Any], blendLookup: dict[str, Any] 84 | ) -> Layer: 85 | """Grab an image from .layered.""" 86 | with zipFile.open(f"data/{layer['name']}.png") as layerImage: 87 | image = Image.open(layerImage).convert("RGBA") 88 | return Layer( 89 | name=layer["name"], 90 | image=image, 91 | dimensions=layer["dimensions"], 92 | offsets=layer["offsets"], 93 | opacity=layer["opacity"], 94 | visible=layer["visible"], 95 | blendmode=blendModeLookup(layer["blendmode"], blendLookup), 96 | ) 97 | 98 | 99 | def saveLayer_LAYERED(fileName: str, layeredImage: LayeredImage) -> None: 100 | """Save a layered image as .layered.""" 101 | _saveLayer_LAYERED(fileName, layeredImage) 102 | 103 | 104 | def _saveLayer_LAYERED( 105 | fileName: str, layeredImage: LayeredImage, *, compressed: bool = False 106 | ) -> None: 107 | """Save a layered image as .layered.""" 108 | with zipfile.ZipFile( 109 | fileName, "w", compression=(zipfile.ZIP_DEFLATED if compressed else zipfile.ZIP_STORED) 110 | ) as layered: 111 | layered.writestr( 112 | "stack.json", json.dumps(layeredImage.json(), indent=(None if compressed else True)) 113 | ) 114 | for layer in layeredImage.layers: 115 | writeImage_LAYERED( 116 | layer.image, layered, f"data/{layer.name}.png", compressed=compressed 117 | ) 118 | compositeImage = layeredImage.getFlattenLayers() 119 | thumbnail = compositeImage.copy() 120 | thumbnail.thumbnail((256, 256)) 121 | imageLookup = {"composite": compositeImage, "thumbnail": thumbnail} 122 | for imageName, imageData in imageLookup.items(): 123 | writeImage_LAYERED(imageData, layered, f"{imageName}.png", compressed=compressed) 124 | 125 | 126 | def writeImage_LAYERED( 127 | image: Image.Image, zipFile: ZipFile, path: str, *, compressed: bool = False 128 | ) -> None: 129 | """Write an image to the archive.""" 130 | imgByteArr = io.BytesIO() 131 | imageCopy = image.copy() 132 | paletteSize = 256 133 | if compressed and len(set(imageCopy.getcolors(maxcolors=paletteSize**3))) < paletteSize: 134 | imageCopy = imageCopy.quantize(colors=paletteSize, method=2, kmeans=1) 135 | imageCopy.save(imgByteArr, format="PNG", optimize=compressed) 136 | imgByteArr.seek(0) 137 | zipFile.writestr(path, imgByteArr.read()) 138 | 139 | 140 | ## LAYEREDC ## 141 | def openLayer_LAYEREDC(file: str) -> LayeredImage: 142 | """Open a .layeredc file into a layered image.""" 143 | return openLayer_LAYERED(file) 144 | 145 | 146 | def saveLayer_LAYEREDC(fileName: str, layeredImage: LayeredImage) -> None: 147 | """Save a layeredc image as .layered.""" 148 | _saveLayer_LAYERED(fileName, layeredImage, compressed=True) 149 | -------------------------------------------------------------------------------- /layeredimage/io/lsr.py: -------------------------------------------------------------------------------- 1 | """Do file io - LSR.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pathlib import Path 6 | 7 | from layeredimage.io.common import expandLayer 8 | from layeredimage.layeredimage import LayeredImage 9 | from layeredimage.layergroup import Group, Layer 10 | 11 | 12 | ## LSR ## 13 | def openLayer_LSR(file: str) -> LayeredImage: 14 | """Open a .lsr file into a layered image.""" 15 | import pylsr 16 | 17 | project = pylsr.read(file) 18 | groups = [] 19 | for group in project.layers: 20 | groups.append( 21 | Group( 22 | name=group.name, 23 | layers=[ 24 | Layer( 25 | name=layer.name, 26 | image=layer.scaledImage(), 27 | dimensions=layer.scaledImage().size, 28 | ) 29 | for layer in group.images 30 | ], 31 | dimensions=group.size, 32 | offsets=(int(group.offsets()[0]), int(group.offsets()[1])), 33 | ) 34 | ) 35 | return LayeredImage(groups, project.size) 36 | 37 | 38 | def saveLayer_LSR(fileName: str, layeredImage: LayeredImage) -> None: 39 | """Save a layered image as .lsr.""" 40 | import pylsr 41 | 42 | layers = [] 43 | for group in layeredImage.layersAndGroups: 44 | if isinstance(group, Layer): 45 | imageData = [pylsr.LSRImageData(group.image, group.name)] 46 | else: 47 | imageData = [ 48 | pylsr.LSRImageData( 49 | expandLayer( 50 | dimensions=group.dimensions, 51 | foreground=layer.image, 52 | opacity=layer.opacity, 53 | offsets=layer.offsets, 54 | ), 55 | layer.name, 56 | ) 57 | for layer in group.layers 58 | ] 59 | layers.append( 60 | pylsr.LSRLayer( 61 | imageData, 62 | group.name, 63 | group.dimensions, 64 | ( 65 | group.offsets[0] + group.dimensions[0] // 2, 66 | group.offsets[1] + group.dimensions[1] // 2, 67 | ), 68 | ) 69 | ) 70 | lsrImage = pylsr.LSRImage( 71 | size=layeredImage.dimensions, name=Path(fileName).name.replace(".lsr", ""), layers=layers 72 | ) 73 | pylsr.write(fileName, lsrImage) 74 | -------------------------------------------------------------------------------- /layeredimage/io/ora.py: -------------------------------------------------------------------------------- 1 | """Do file io - ORA.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Any 6 | 7 | from blendmodes.blend import BlendType 8 | 9 | from layeredimage.io.common import blendModeLookup 10 | from layeredimage.layeredimage import LayeredImage 11 | from layeredimage.layergroup import Group, Layer 12 | 13 | 14 | #### ORA #### 15 | def openLayer_ORA(file: str) -> LayeredImage: 16 | """Open an .ora file into a layered image.""" 17 | from pyora import TYPE_LAYER, Project 18 | 19 | blendLookup = { 20 | "svg:src-over": BlendType.NORMAL, 21 | "svg:multiply": BlendType.MULTIPLY, 22 | "svg:color-burn": BlendType.COLOURBURN, 23 | "svg:color-dodge": BlendType.COLOURDODGE, 24 | "svg:": BlendType.REFLECT, 25 | "svg:overlay": BlendType.OVERLAY, 26 | "svg:difference": BlendType.DIFFERENCE, 27 | "svg:lighten": BlendType.LIGHTEN, 28 | "svg:darken": BlendType.DARKEN, 29 | "svg:screen": BlendType.SCREEN, 30 | "svg:hard-light": BlendType.HARDLIGHT, 31 | "svg:soft-light": BlendType.SOFTLIGHT, 32 | "svg:hue": BlendType.HUE, 33 | "svg:saturation": BlendType.SATURATION, 34 | "svg:color": BlendType.COLOUR, 35 | "svg:luminosity": BlendType.LUMINOSITY, 36 | "svg:plus": BlendType.ADDITIVE, 37 | "svg:dst-in": BlendType.DESTIN, 38 | "svg:dst-out": BlendType.DESTOUT, 39 | "svg:dst-atop": BlendType.DESTATOP, 40 | "svg:src-atop": BlendType.SRCATOP, 41 | } 42 | layersAndGroups = [] 43 | project = Project.load(file) 44 | for layerOrGroup in project.children[::-1]: 45 | if layerOrGroup.type == TYPE_LAYER: 46 | layersAndGroups.append( 47 | Layer( 48 | name=layerOrGroup.name, 49 | image=layerOrGroup.get_image_data(raw=True), 50 | dimensions=layerOrGroup.dimensions, 51 | offsets=layerOrGroup.offsets, 52 | opacity=layerOrGroup.opacity, 53 | visible=layerOrGroup.visible, 54 | blendmode=blendModeLookup(layerOrGroup.composite_op, blendLookup), 55 | ) 56 | ) 57 | else: 58 | layers = [] 59 | for layer in list(layerOrGroup.children)[::-1]: 60 | layers.append( 61 | Layer( 62 | name=layer.name, 63 | image=layer.get_image_data(raw=True), 64 | dimensions=layer.dimensions, 65 | offsets=layer.offsets, 66 | opacity=layer.opacity, 67 | visible=layer.visible, 68 | blendmode=blendModeLookup(layerOrGroup.composite_op, blendLookup), 69 | ) 70 | ) 71 | layersAndGroups.append( 72 | Group( 73 | name=layerOrGroup.name, 74 | layers=layers, 75 | dimensions=project.dimensions, 76 | offsets=layerOrGroup.offsets, 77 | opacity=layerOrGroup.opacity, 78 | visible=layerOrGroup.visible, 79 | blendmode=blendModeLookup(layerOrGroup.composite_op, blendLookup), 80 | ) 81 | ) 82 | return LayeredImage(layersAndGroups, project.dimensions) 83 | 84 | 85 | def saveLayer_ORA(fileName: str, layeredImage: LayeredImage) -> None: 86 | """Save a layered image as .ora.""" 87 | from pyora import Project 88 | 89 | blendLookup = { 90 | BlendType.NORMAL: "svg:src-over", 91 | BlendType.MULTIPLY: "svg:multiply", 92 | BlendType.COLOURBURN: "svg:color-burn", 93 | BlendType.COLOURDODGE: "svg:color-dodge", 94 | BlendType.REFLECT: "svg:", 95 | BlendType.OVERLAY: "svg:overlay", 96 | BlendType.DIFFERENCE: "svg:difference", 97 | BlendType.LIGHTEN: "svg:lighten", 98 | BlendType.DARKEN: "svg:darken", 99 | BlendType.SCREEN: "svg:screen", 100 | BlendType.SOFTLIGHT: "svg:soft-light", 101 | BlendType.HARDLIGHT: "svg:hard-light", 102 | BlendType.HUE: "svg:hue", 103 | BlendType.SATURATION: "svg:saturation", 104 | BlendType.COLOUR: "svg:color", 105 | BlendType.LUMINOSITY: "svg:luminosity", 106 | BlendType.ADDITIVE: "svg:plus", 107 | BlendType.DESTIN: "svg:dst-in", 108 | BlendType.DESTOUT: "svg:dst-out", 109 | BlendType.DESTATOP: "svg:dst-atop", 110 | BlendType.SRCATOP: "svg:src-atop", 111 | } 112 | project = Project.new(layeredImage.dimensions[0], layeredImage.dimensions[1]) 113 | for layerOrGroup in layeredImage.layersAndGroups: 114 | if isinstance(layerOrGroup, Layer): 115 | project = addLayer_ORA(project, layerOrGroup, blendLookup) 116 | else: 117 | group = project.add_group( 118 | layerOrGroup.name, 119 | offsets=layerOrGroup.offsets, 120 | opacity=layerOrGroup.opacity, 121 | visible=layerOrGroup.visible, 122 | composite_op=blendModeLookup(layerOrGroup.blendmode, blendLookup, "svg:src-over"), 123 | ) 124 | for layer in layerOrGroup.layers: 125 | group = addLayer_ORA(group, layer, blendLookup) 126 | project.save(fileName) 127 | 128 | 129 | def addLayer_ORA(project: Any, layer: Any, blendLookup: dict[BlendType, str]) -> Any: 130 | """Update the project with a shiny new layer.""" 131 | project.add_layer( 132 | layer.image, 133 | layer.name, 134 | offsets=layer.offsets, 135 | opacity=layer.opacity, 136 | visible=layer.visible, 137 | composite_op=blendModeLookup(layer.blendmode, blendLookup, "svg:src-over"), 138 | ) 139 | return project 140 | -------------------------------------------------------------------------------- /layeredimage/io/pdn.py: -------------------------------------------------------------------------------- 1 | """Do file io - PDN.""" 2 | 3 | from __future__ import annotations 4 | 5 | from blendmodes.blend import BlendType 6 | from loguru import logger 7 | from PIL import Image 8 | 9 | from layeredimage.io.common import blendModeLookup 10 | from layeredimage.layeredimage import LayeredImage 11 | from layeredimage.layergroup import Layer 12 | 13 | 14 | #### PDN #### 15 | def openLayer_PDN(file: str) -> LayeredImage: 16 | """Open a .pdn file into a layered image.""" 17 | from pypdn.reader import BlendType as PDNBlend 18 | from pypdn.reader import read 19 | 20 | blendLookup = { 21 | PDNBlend.Normal: BlendType.NORMAL, 22 | PDNBlend.Multiply: BlendType.MULTIPLY, 23 | PDNBlend.Additive: BlendType.ADDITIVE, 24 | PDNBlend.ColorBurn: BlendType.COLOURBURN, 25 | PDNBlend.ColorDodge: BlendType.COLOURDODGE, 26 | PDNBlend.Reflect: BlendType.REFLECT, 27 | PDNBlend.Glow: BlendType.GLOW, 28 | PDNBlend.Overlay: BlendType.OVERLAY, 29 | PDNBlend.Difference: BlendType.DIFFERENCE, 30 | PDNBlend.Negation: BlendType.NEGATION, 31 | PDNBlend.Lighten: BlendType.LIGHTEN, 32 | PDNBlend.Darken: BlendType.DARKEN, 33 | PDNBlend.Screen: BlendType.SCREEN, 34 | PDNBlend.XOR: BlendType.XOR, 35 | } 36 | project = read(file) 37 | layers = [] 38 | for layer in project.layers: 39 | image = Image.fromarray(layer.image) 40 | layers.append( 41 | Layer( 42 | name=layer.name, 43 | image=image, 44 | dimensions=image.size, 45 | offsets=(0, 0), 46 | opacity=layer.opacity / 255, 47 | visible=layer.visible, 48 | blendmode=blendModeLookup(layer.blendMode, blendLookup), 49 | ) 50 | ) 51 | return LayeredImage(layers, (project.width, project.height)) 52 | 53 | 54 | def saveLayer_PDN(fileName: str, layeredImage: LayeredImage) -> None: 55 | """Save a layered image as .pdn.""" 56 | del fileName, layeredImage 57 | logger.error("Saving PDNs is not implemented in pypdn") 58 | raise NotImplementedError 59 | -------------------------------------------------------------------------------- /layeredimage/io/psd.py: -------------------------------------------------------------------------------- 1 | """Do file io - PSD.""" 2 | 3 | from __future__ import annotations 4 | 5 | from blendmodes.blend import BlendType 6 | from loguru import logger 7 | 8 | from layeredimage.io.common import blendModeLookup 9 | from layeredimage.layeredimage import LayeredImage 10 | from layeredimage.layergroup import Group, Layer 11 | 12 | 13 | #### PSD #### 14 | def openLayer_PSD(file: str) -> LayeredImage: 15 | """Open a .psd file into a layered image.""" 16 | from psd_tools import PSDImage 17 | from psd_tools.constants import BlendMode as psdB 18 | 19 | blendLookup = { 20 | psdB.NORMAL: BlendType.NORMAL, 21 | psdB.MULTIPLY: BlendType.MULTIPLY, 22 | psdB.COLOR_BURN: BlendType.COLOURBURN, 23 | psdB.COLOR_DODGE: BlendType.COLOURDODGE, 24 | psdB.OVERLAY: BlendType.OVERLAY, 25 | psdB.DIFFERENCE: BlendType.DIFFERENCE, 26 | psdB.SUBTRACT: BlendType.NEGATION, 27 | psdB.LIGHTEN: BlendType.LIGHTEN, 28 | psdB.DARKEN: BlendType.DARKEN, 29 | psdB.SCREEN: BlendType.SCREEN, 30 | psdB.SOFT_LIGHT: BlendType.SOFTLIGHT, 31 | psdB.HARD_LIGHT: BlendType.HARDLIGHT, 32 | psdB.EXCLUSION: BlendType.EXCLUSION, 33 | psdB.HUE: BlendType.HUE, 34 | psdB.SATURATION: BlendType.SATURATION, 35 | psdB.COLOR: BlendType.COLOUR, 36 | psdB.LUMINOSITY: BlendType.LUMINOSITY, 37 | psdB.DIVIDE: BlendType.DIVIDE, 38 | psdB.PIN_LIGHT: BlendType.PINLIGHT, 39 | psdB.VIVID_LIGHT: BlendType.VIVIDLIGHT, 40 | } 41 | layersAndGroups = [] 42 | project = PSDImage.open(file) 43 | for layerOrGroup in project: 44 | if layerOrGroup.is_group(): 45 | layers = [] 46 | for layer in layerOrGroup: 47 | layers.append( 48 | Layer( 49 | name=layer.name, 50 | image=layer.topil(), 51 | dimensions=(layer.width, layer.height), 52 | offsets=(layer.left - layerOrGroup.left, layer.top - layerOrGroup.top), 53 | opacity=layer.opacity / 255, 54 | visible=layer.visible, 55 | blendmode=blendModeLookup(layer.blend_mode, blendLookup), 56 | ) 57 | ) 58 | layersAndGroups.append( 59 | Group( 60 | name=layerOrGroup.name, 61 | layers=layers, 62 | dimensions=(layerOrGroup.width, layerOrGroup.height), 63 | offsets=(layerOrGroup.left, layerOrGroup.top), 64 | opacity=layerOrGroup.opacity / 255, 65 | visible=layerOrGroup.visible, 66 | blendmode=blendModeLookup(layerOrGroup.blend_mode, blendLookup), 67 | ) 68 | ) 69 | else: 70 | layersAndGroups.append( 71 | Layer( 72 | name=layerOrGroup.name, 73 | image=layerOrGroup.topil(), 74 | dimensions=(layerOrGroup.width, layerOrGroup.height), 75 | offsets=(layerOrGroup.left, layerOrGroup.top), 76 | opacity=layerOrGroup.opacity / 255, 77 | visible=layerOrGroup.visible, 78 | blendmode=blendModeLookup(layerOrGroup.blend_mode, blendLookup), 79 | ) 80 | ) 81 | return LayeredImage(layersAndGroups, (project.width, project.height)) 82 | 83 | 84 | def saveLayer_PSD(fileName: str, layeredImage: LayeredImage) -> None: 85 | """Save a layered image as .psd.""" 86 | del fileName, layeredImage 87 | logger.error("Saving PSDs is not implemented in psd-tools3") 88 | raise NotImplementedError 89 | -------------------------------------------------------------------------------- /layeredimage/io/tiff.py: -------------------------------------------------------------------------------- 1 | """Do file io - TIFF.""" 2 | 3 | from __future__ import annotations 4 | 5 | from PIL import Image 6 | 7 | from layeredimage.io.common import expandLayersToCanvas 8 | from layeredimage.layeredimage import LayeredImage 9 | from layeredimage.layergroup import Layer 10 | 11 | 12 | #### TIFF #### 13 | def openLayer_TIFF(file: str) -> LayeredImage: 14 | """Open a .tiff or a .tif file into a layered image.""" 15 | project = Image.open(file) 16 | layers = [] 17 | dimensions = [0, 0] 18 | for index in range(project.n_frames): 19 | # Load the correct image 20 | project.seek(index) 21 | # Update the project dimensions 22 | for indx, dimension in enumerate(dimensions): 23 | if project.size[indx] > dimension: 24 | dimensions[indx] = project.size[indx] 25 | ifd = project.ifd.named() 26 | # Offsets 27 | offsetX = 0 28 | offsetY = 0 29 | if "XPosition" in ifd: 30 | offsetX = int( 31 | ifd["XPosition"][0][0] / ifd["XPosition"][0][1] * ifd["XResolution"][0][0] 32 | ) 33 | if "YPosition" in ifd: 34 | offsetY = int( 35 | ifd["YPosition"][0][0] / ifd["YPosition"][0][1] * ifd["YResolution"][0][0] 36 | ) 37 | # Add the layer 38 | layers.append( 39 | Layer( 40 | name=ifd["PageName"][0], 41 | image=project.copy(), 42 | dimensions=(ifd["ImageWidth"][0], ifd["ImageLength"][0]), 43 | offsets=(offsetX, offsetY), 44 | opacity=1, 45 | visible=True, 46 | ) 47 | ) 48 | project.close() 49 | return LayeredImage(layers, (dimensions[0], dimensions[1])) 50 | 51 | 52 | def saveLayer_TIFF(fileName: str, layeredImage: LayeredImage) -> None: 53 | """Save a layered image as .tiff or .tif.""" 54 | layers = expandLayersToCanvas(layeredImage, "TIFF") 55 | layers[0].save(fileName, compression=None, save_all=True, append_images=layers[1:]) 56 | -------------------------------------------------------------------------------- /layeredimage/io/webp.py: -------------------------------------------------------------------------------- 1 | """Do file io - WEBP.""" 2 | 3 | from __future__ import annotations 4 | 5 | from PIL import Image 6 | 7 | from layeredimage.io.common import expandLayersToCanvas 8 | from layeredimage.layeredimage import LayeredImage 9 | from layeredimage.layergroup import Layer 10 | 11 | 12 | ## WEBP ## 13 | def openLayer_WEBP(file: str) -> LayeredImage: 14 | """Open a .webp file into a layered image.""" 15 | project = Image.open(file) 16 | projectSize = project.size 17 | layers = [] 18 | for index in range(project.n_frames): 19 | project.seek(index) 20 | layers.append( 21 | Layer(name=f"Frame {len(layers) + 1}", image=project.copy(), dimensions=projectSize) 22 | ) 23 | project.close() 24 | return LayeredImage(layers, projectSize) 25 | 26 | 27 | def saveLayer_WEBP(fileName: str, layeredImage: LayeredImage) -> None: 28 | """Save a layered image as .webp.""" 29 | layers = expandLayersToCanvas(layeredImage, "WEBP") 30 | layers[0].save(fileName, duration=200, save_all=True, append_images=layers[1:]) 31 | -------------------------------------------------------------------------------- /layeredimage/io/xcf.py: -------------------------------------------------------------------------------- 1 | """Do file io - XCF.""" 2 | 3 | from __future__ import annotations 4 | 5 | from blendmodes.blend import BlendType 6 | from loguru import logger 7 | 8 | from layeredimage.io.common import blendModeLookup 9 | from layeredimage.layeredimage import LayeredImage 10 | from layeredimage.layergroup import Group, Layer 11 | 12 | 13 | #### XCF #### 14 | def openLayer_XCF(file: str) -> LayeredImage: 15 | """Open an .xcf file into a layered image.""" 16 | from gimpformats.gimpXcfDocument import GimpDocument, GimpGroup 17 | 18 | blendLookup = { 19 | 0: BlendType.NORMAL, 20 | 3: BlendType.MULTIPLY, 21 | 4: BlendType.SCREEN, 22 | 5: BlendType.OVERLAY, 23 | 6: BlendType.DIFFERENCE, 24 | 7: BlendType.ADDITIVE, 25 | 8: BlendType.NEGATION, 26 | 9: BlendType.DARKEN, 27 | 10: BlendType.LIGHTEN, 28 | 11: BlendType.HUE, 29 | 12: BlendType.SATURATION, 30 | 13: BlendType.COLOUR, 31 | 14: BlendType.LUMINOSITY, 32 | 15: BlendType.DIVIDE, 33 | 16: BlendType.COLOURDODGE, 34 | 17: BlendType.COLOURBURN, 35 | 18: BlendType.HARDLIGHT, 36 | 19: BlendType.SOFTLIGHT, 37 | 20: BlendType.GRAINEXTRACT, 38 | 21: BlendType.GRAINMERGE, 39 | 23: BlendType.OVERLAY, 40 | 24: BlendType.HUE, 41 | 25: BlendType.SATURATION, 42 | 26: BlendType.COLOUR, 43 | 27: BlendType.LUMINOSITY, 44 | 28: BlendType.NORMAL, 45 | 30: BlendType.MULTIPLY, 46 | 31: BlendType.SCREEN, 47 | 32: BlendType.DIFFERENCE, 48 | 33: BlendType.ADDITIVE, 49 | 34: BlendType.NEGATION, 50 | 35: BlendType.DARKEN, 51 | 36: BlendType.LIGHTEN, 52 | 37: BlendType.HUE, 53 | 38: BlendType.SATURATION, 54 | 39: BlendType.COLOUR, 55 | 40: BlendType.LUMINOSITY, 56 | 41: BlendType.DIVIDE, 57 | 42: BlendType.COLOURDODGE, 58 | 43: BlendType.COLOURBURN, 59 | 44: BlendType.HARDLIGHT, 60 | 45: BlendType.SOFTLIGHT, 61 | 46: BlendType.GRAINEXTRACT, 62 | 47: BlendType.GRAINMERGE, 63 | 48: BlendType.VIVIDLIGHT, 64 | 49: BlendType.PINLIGHT, 65 | 52: BlendType.EXCLUSION, 66 | } 67 | project = GimpDocument(file) 68 | # Iterate the layers and create a list of layers for each group, then remove 69 | # these from the project layers 70 | 71 | root_group = project.walkTree() 72 | 73 | def rec_walk(group: GimpGroup) -> list[Group | Layer]: 74 | layers = [] 75 | for child in group.children[::-1]: 76 | if isinstance(child, GimpGroup): 77 | info = child.layer_options 78 | if info is not None: 79 | layers.append( 80 | Group( 81 | name=str(info.name), 82 | layers=rec_walk(child), 83 | dimensions=(info.width, info.height), 84 | offsets=(0, 0), 85 | opacity=info.opacity, 86 | visible=info.visible, 87 | blendmode=blendModeLookup(info.blendMode, blendLookup), 88 | ) 89 | ) 90 | else: 91 | layers.append( 92 | Layer( 93 | name=str(child.name), 94 | image=child.image, 95 | dimensions=(child.width, child.height), 96 | offsets=( 97 | child.xOffset, 98 | child.yOffset, 99 | ), 100 | opacity=child.opacity, 101 | visible=child.visible, 102 | blendmode=blendModeLookup(child.blendMode, blendLookup), 103 | ) 104 | ) 105 | return layers 106 | 107 | layersAndGroups = rec_walk(root_group) 108 | 109 | return LayeredImage(layersAndGroups, (project.width, project.height)) 110 | 111 | 112 | def saveLayer_XCF(fileName: str, layeredImage: LayeredImage) -> None: 113 | """Save a layered image as .xcf.""" 114 | del fileName, layeredImage 115 | logger.error( 116 | "Saving XCFs is not implemented in gimpformats - " 117 | "this is a little misleading as functions are present, however these are not " 118 | "functional" 119 | ) 120 | raise NotImplementedError 121 | -------------------------------------------------------------------------------- /layeredimage/layeredimage.py: -------------------------------------------------------------------------------- 1 | """LayeredImage class.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Any 6 | 7 | import numpy as np 8 | from blendmodes.blend import blendLayersArray 9 | from PIL import Image 10 | 11 | from layeredimage.layergroup import Group, Layer 12 | 13 | 14 | class LayeredImage: 15 | """A representation of a layered image such as an ora.""" 16 | 17 | def __init__( 18 | self, 19 | layersAndGroups: list[Layer | Group], 20 | dimensions: tuple[int, int] | None = None, 21 | **kwargs: dict[str, Any], 22 | ) -> None: 23 | """LayeredImage - representation of a layered image. 24 | 25 | Args: 26 | ---- 27 | layersAndGroups (list[Layer, Group]): List of layers and groups 28 | dimensions (tuple[int, int], optional): dimensions of the canvas. Defaults to None. 29 | **kwargs (Any): add any keyword args to self.extras 30 | 31 | """ 32 | # Write here 33 | self.layersAndGroups = layersAndGroups 34 | # Read only 35 | self.groups = self.extractGroups() 36 | self.layers = self.extractLayers() 37 | # If the user does not specify the dimensions use the largest x and y of 38 | # the layers and groups, include offsets 39 | self.dimensions = dimensions or (0, 0) 40 | lyrOrGrpX = [lyrOrGrp.dimensions[0] + lyrOrGrp.offsets[0] for lyrOrGrp in layersAndGroups] 41 | lyrOrGrpY = [lyrOrGrp.dimensions[1] + lyrOrGrp.offsets[1] for lyrOrGrp in layersAndGroups] 42 | if dimensions is None: 43 | self.dimensions = ( 44 | max(lyrOrGrpX or [0]), 45 | max(lyrOrGrpY or [0]), 46 | ) 47 | self.extras = kwargs 48 | 49 | def __repr__(self) -> str: 50 | """Get the string representation.""" 51 | return self.__str__() 52 | 53 | def __str__(self) -> str: 54 | """Get the string representation.""" 55 | return ( 56 | f"" 58 | ) 59 | 60 | def json(self) -> dict[str, Any]: 61 | """Get the object as a dict.""" 62 | layersAndGroups = [layerOrGroup.json() for layerOrGroup in self.layersAndGroups] 63 | return {"dimensions": self.dimensions, "layersAndGroups": layersAndGroups} 64 | 65 | # Get, set and remove layers or groups 66 | def getLayerOrGroup(self, index: int) -> Layer | Group: 67 | """Get a LayerOrGroup.""" 68 | return self.layersAndGroups[index] 69 | 70 | def addLayerOrGroup(self, layerOrGroup: Layer | Group) -> None: 71 | """Add a LayerOrGroup.""" 72 | self.layersAndGroups.append(layerOrGroup) 73 | 74 | def insertLayerOrGroup(self, layerOrGroup: Layer | Group, index: int) -> None: 75 | """Insert a LayerOrGroup at a specific index.""" 76 | self.layersAndGroups.insert(index, layerOrGroup) 77 | 78 | def removeLayerOrGroup(self, index: int) -> None: 79 | """Remove a LayerOrGroup at a specific index.""" 80 | self.layersAndGroups.pop(index) 81 | 82 | # The user may want to flatten the layers 83 | def getFlattenLayers(self) -> Image.Image: 84 | """Return an image for all flattened layers.""" 85 | 86 | project_image = np.zeros((self.dimensions[1], self.dimensions[0], 4), dtype=np.uint8) 87 | for layerOrGroup in self.layersAndGroups: 88 | if layerOrGroup.visible: 89 | project_image = render(layerOrGroup, project_image) 90 | 91 | return Image.fromarray(np.uint8(np.around(project_image, 0))) 92 | 93 | # The user may hate groups and just want the layers... or just want the 94 | # groups 95 | def extractLayers(self) -> list[Layer]: 96 | """Extract the layers from the image.""" 97 | layers = [] 98 | for layerOrGroup in self.layersAndGroups: 99 | if isinstance(layerOrGroup, Layer): 100 | layers.append(layerOrGroup) 101 | else: 102 | layers.extend( 103 | [ 104 | Layer( 105 | name=layer.name, 106 | image=layer.image, 107 | dimensions=( 108 | max(layer.dimensions[0], layerOrGroup.dimensions[0]), 109 | max(layer.dimensions[1], layerOrGroup.dimensions[1]), 110 | ), 111 | offsets=( 112 | layerOrGroup.offsets[0] + layer.offsets[0], 113 | layerOrGroup.offsets[1] + layer.offsets[1], 114 | ), 115 | opacity=layerOrGroup.opacity * layer.opacity, 116 | visible=layerOrGroup.visible and layer.visible, 117 | ) 118 | for layer in layerOrGroup.layers 119 | ] 120 | ) 121 | return layers 122 | 123 | def updateLayers(self) -> None: 124 | """Update the layers from the image.""" 125 | self.layers = self.extractLayers() 126 | 127 | def extractGroups(self) -> list[Group]: 128 | """Extract the groups from the image.""" 129 | return [ 130 | _layerOrGroup 131 | for _layerOrGroup in self.layersAndGroups 132 | if isinstance(_layerOrGroup, Group) 133 | ] 134 | 135 | def updateGroups(self) -> None: 136 | """Update the groups from the image.""" 137 | self.groups = self.extractGroups() 138 | 139 | 140 | def render(layerOrGroup: Layer | Group, project_image: np.ndarray) -> np.ndarray: 141 | """Flatten a layer or group on to an image of what has already been flattened. 142 | 143 | Args: 144 | ---- 145 | layerOrGroup (Layer, Group): A layer or a group of layers 146 | project_image (np.ndarray, optional): the image of what has already 147 | been flattened. 148 | 149 | Returns: 150 | ------- 151 | np.ndarray: Flattened image 152 | 153 | """ 154 | if not layerOrGroup.visible: 155 | return project_image 156 | 157 | if isinstance(layerOrGroup, Layer): 158 | return blendLayersArray( 159 | project_image, 160 | layerOrGroup.image, 161 | layerOrGroup.blendmode, 162 | layerOrGroup.opacity, 163 | layerOrGroup.offsets, 164 | ) 165 | if isinstance(layerOrGroup, Group): 166 | group_image = np.zeros( 167 | (layerOrGroup.dimensions[1], layerOrGroup.dimensions[0], 4), dtype=np.uint8 168 | ) 169 | for item in layerOrGroup.layers: 170 | group_image = render(item, group_image) 171 | return blendLayersArray( 172 | project_image, 173 | group_image, 174 | layerOrGroup.blendmode, 175 | layerOrGroup.opacity, 176 | layerOrGroup.offsets, 177 | ) 178 | 179 | msg = "Unsupported type encountered" 180 | raise TypeError(msg) 181 | -------------------------------------------------------------------------------- /layeredimage/layergroup.py: -------------------------------------------------------------------------------- 1 | """Base class.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Any 6 | 7 | from blendmodes.blend import BlendType 8 | from PIL import Image 9 | 10 | 11 | class LayerGroup: 12 | """A representation of an image layer or group.""" 13 | 14 | def __init__( 15 | self, 16 | name: str, 17 | dimensions: tuple[int, int], 18 | offsets: tuple[int, int] = (0, 0), 19 | opacity: float = 1.0, 20 | *, 21 | visible: bool = True, 22 | blendmode: BlendType = BlendType.NORMAL, 23 | **kwargs: dict[str, Any], 24 | ) -> None: 25 | """Represent an image layer or group. 26 | 27 | Args: 28 | ---- 29 | name (str): Name of the layer or group 30 | dimensions ((int, int)): A tuple representing the dimensions in 31 | pixels 32 | offsets (tuple, optional): A tuple representing the left and top 33 | offsets in pixels. Defaults to (0, 0). 34 | opacity (float, optional): A float representing the alpha value 35 | where 0 is invisible and 1 is fully visible. Defaults to 1.0. 36 | visible (bool, optional): Is the layer visible to the user (this 37 | is often configured per layer or per group by an 'eye' icon). 38 | Defaults to True. 39 | blendmode (Blendtype): The blending mode to use. Defaults to BlendType.NORMAL 40 | **kwargs (Any): add any keyword args to self.extras 41 | 42 | """ 43 | self.name = name 44 | self.offsets = offsets 45 | self.opacity = opacity 46 | self.visible = visible 47 | self.dimensions = dimensions 48 | self.blendmode = blendmode 49 | self.extras = kwargs 50 | 51 | def __repr__(self) -> str: 52 | """Get the string representation.""" 53 | return self.__str__() 54 | 55 | def __str__(self) -> str: 56 | """Get the string representation.""" 57 | return f'' 58 | 59 | def json(self) -> dict[str, Any]: 60 | """Get the object as a dict.""" 61 | return { 62 | "name": self.name, 63 | "offsets": self.offsets, 64 | "opacity": self.opacity, 65 | "visible": self.visible, 66 | "dimensions": self.dimensions, 67 | "blendmode": self.blendmode.name, 68 | } 69 | 70 | 71 | class Layer(LayerGroup): 72 | """A representation of an image layer.""" 73 | 74 | def __init__( 75 | self, 76 | name: str, 77 | image: Image.Image, 78 | dimensions: tuple[int, int] | None = None, 79 | offsets: tuple[int, int] = (0, 0), 80 | opacity: float = 1.0, 81 | *, 82 | visible: bool = True, 83 | blendmode: BlendType = BlendType.NORMAL, 84 | ) -> None: 85 | """Representation of an image layer. 86 | 87 | Args: 88 | ---- 89 | name (str): Name of the layer or group 90 | image (Image.Image): A PIL Image 91 | dimensions (tuple[int, int]): A tuple representing the dimensions in 92 | pixels 93 | offsets (tuple[int, int], optional): A tuple representing the left and top 94 | offsets in pixels. Defaults to (0, 0). 95 | opacity (float, optional): A float representing the alpha value 96 | where 0 is invisible and 1 is fully visible. Defaults to 1.0. 97 | visible (bool, optional): Is the layer visible to the user (this 98 | is often configured per layer or per group by an 'eye' icon). 99 | Defaults to True. 100 | blendmode (Blendtype): The blending mode to use. Defaults to BlendType.NORMAL 101 | 102 | """ 103 | super().__init__( 104 | name, (0, 0), offsets=offsets, opacity=opacity, visible=visible, blendmode=blendmode 105 | ) 106 | self.image = image 107 | 108 | # If the user does not specify the dimensions use image.size 109 | self.dimensions = dimensions or image.size 110 | 111 | def json(self) -> dict[str, Any]: 112 | """Get the object as a dict.""" 113 | return { 114 | "name": self.name, 115 | "offsets": self.offsets, 116 | "opacity": self.opacity, 117 | "visible": self.visible, 118 | "dimensions": self.dimensions, 119 | "type": "LAYER", 120 | "blendmode": self.blendmode.name, 121 | } 122 | 123 | 124 | class Group(LayerGroup): 125 | """A representation of an image group.""" 126 | 127 | def __init__( 128 | self, 129 | name: str, 130 | layers: list[Layer], 131 | dimensions: tuple[int, int] | None = None, 132 | offsets: tuple[int, int] = (0, 0), 133 | opacity: float = 1.0, 134 | *, 135 | visible: bool = True, 136 | blendmode: BlendType = BlendType.NORMAL, 137 | ) -> None: 138 | """Representation of an image group. 139 | 140 | Args: 141 | ---- 142 | name (str): Name of the layer or group 143 | layers (layeredimage.Layer[]): A list of layers where the next 144 | index stacks upon the previous layer 145 | dimensions ((int, int)): A tuple representing the dimensions in 146 | pixels 147 | offsets (tuple, optional): A tuple representing the left and top 148 | offsets in pixels. Defaults to (0, 0). 149 | opacity (float, optional): A float representing the alpha value 150 | where 0 is invisible and 1 is fully visible. Defaults to 1.0. 151 | visible (bool, optional): Is the layer visible to the user (this 152 | is often configured per layer or per group by an 'eye' icon). 153 | Defaults to True. 154 | blendmode (Blendtype): The blending mode to use. Defaults to BlendType.NORMAL 155 | 156 | """ 157 | # Initialise dimens to 0 and then calculate as below 158 | super().__init__( 159 | name, (0, 0), offsets=offsets, opacity=opacity, visible=visible, blendmode=blendmode 160 | ) 161 | self.layers = layers 162 | 163 | # If the user does not specify the dimensions use the largest x and y of 164 | # the layers 165 | self.dimensions = dimensions or (0, 0) 166 | if dimensions is None: 167 | self.dimensions = ( 168 | max(layer.dimensions[0] + layer.offsets[0] for layer in layers), 169 | max(layer.dimensions[1] + layer.offsets[1] for layer in layers), 170 | ) 171 | 172 | def json(self) -> dict[str, Any]: 173 | """Get the object as a dict.""" 174 | layers = [layer.json() for layer in self.layers] 175 | return { 176 | "name": self.name, 177 | "offsets": self.offsets, 178 | "opacity": self.opacity, 179 | "visible": self.visible, 180 | "dimensions": self.dimensions, 181 | "type": "GROUP", 182 | "blendmode": self.blendmode.name, 183 | "layers": layers, 184 | } 185 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "layeredimage" 3 | version = "2025" 4 | description = "Use this module to read, and write to a number of layered image formats" 5 | authors = [{ name = "FredHappyface" }] 6 | requires-python = ">=3.9,<4.0" 7 | readme = "README.md" 8 | license = "mit" 9 | classifiers = [ 10 | "Environment :: Console", 11 | "Development Status :: 5 - Production/Stable", 12 | "Intended Audience :: Developers", 13 | "Intended Audience :: Education", 14 | "Natural Language :: English", 15 | "Operating System :: OS Independent", 16 | "Programming Language :: Python :: Implementation :: CPython", 17 | "Topic :: Multimedia :: Graphics", 18 | "Topic :: Software Development :: Libraries :: Python Modules", 19 | "Topic :: Utilities", 20 | ] 21 | dependencies = [ 22 | "blendmodes>=2025", 23 | "gimpformats>=2025", 24 | "loguru>=0.7.3", 25 | "pillow>=10.4.0", 26 | "psd-tools>=1.10.6", 27 | "pylsr>=2024", 28 | "pyora>=0.3.11", 29 | "pypdn>=1.0.6", 30 | ] 31 | 32 | [project.urls] 33 | Homepage = "https://github.com/FHPythonUtils/LayeredImage" 34 | Repository = "https://github.com/FHPythonUtils/LayeredImage" 35 | Documentation = "https://github.com/FHPythonUtils/LayeredImage/blob/master/README.md" 36 | 37 | [dependency-groups] 38 | dev = [ 39 | "coverage>=7.6.12", 40 | "handsdown>=2.1.0", 41 | "imgcompare>=2.0.1", 42 | "pyright>=1.1.394", 43 | "pytest>=8.3.4", 44 | "ruff>=0.9.7", 45 | "safety>=3.3.0", 46 | ] 47 | 48 | [tool.ruff] 49 | line-length = 100 50 | indent-width = 4 51 | target-version = "py38" 52 | 53 | [tool.ruff.lint] 54 | select = ["ALL"] 55 | ignore = [ 56 | "COM812", # enforce trailing comma 57 | "D2", # pydocstyle formatting 58 | "ISC001", 59 | "N", # pep8 naming 60 | "PLR09", # pylint refactor too many 61 | "TCH", # type check blocks 62 | "W191" # ignore this to allow tabs 63 | ] 64 | fixable = ["ALL"] 65 | 66 | [tool.ruff.lint.per-file-ignores] 67 | "**/{tests,docs,tools}/*" = ["D", "S101", "E402"] 68 | 69 | [tool.ruff.lint.flake8-tidy-imports] 70 | ban-relative-imports = "all" # Disallow all relative imports. 71 | 72 | [tool.ruff.format] 73 | indent-style = "tab" 74 | docstring-code-format = true 75 | line-ending = "lf" 76 | 77 | [tool.pyright] 78 | venvPath = "." 79 | venv = ".venv" 80 | 81 | [tool.coverage.run] 82 | branch = true 83 | 84 | [tool.tox] 85 | legacy_tox_ini = """ 86 | [tox] 87 | env_list = 88 | py311 89 | py310 90 | py39 91 | py38 92 | 93 | [testenv] 94 | deps = 95 | imgcompare 96 | pytest 97 | commands = pytest tests 98 | """ 99 | 100 | [build-system] 101 | requires = ["hatchling"] 102 | build-backend = "hatchling.build" 103 | -------------------------------------------------------------------------------- /readme-assets/icons/LayeredImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/readme-assets/icons/LayeredImage.png -------------------------------------------------------------------------------- /readme-assets/icons/LayeredImage.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/readme-assets/icons/LayeredImage.xcf -------------------------------------------------------------------------------- /readme-assets/icons/name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/readme-assets/icons/name.png -------------------------------------------------------------------------------- /readme-assets/icons/proj-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/readme-assets/icons/proj-icon.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv export --no-hashes --no-dev -o requirements.txt 3 | -e . 4 | aenum==3.1.15 5 | aggdraw==1.3.19 6 | attrs==25.1.0 7 | blendmodes==2025 8 | brackettree==0.2.5 9 | colorama==0.4.6 ; sys_platform == 'win32' 10 | defusedxml==0.7.1 11 | deprecation==2.1.0 12 | docopt==0.6.2 13 | gimpformats==2025 14 | imageio==2.37.0 15 | lazy-loader==0.4 16 | loguru==0.7.3 17 | networkx==3.2.1 ; python_full_version < '3.10' 18 | networkx==3.4.2 ; python_full_version >= '3.10' 19 | numpy==2.0.2 ; python_full_version < '3.10' 20 | numpy==2.2.3 ; python_full_version >= '3.10' 21 | packaging==24.2 22 | pillow==10.4.0 23 | psd-tools==1.10.6 24 | pylsr==2024 25 | pyora==0.3.11 26 | pypdn==1.0.6 27 | scikit-image==0.24.0 ; python_full_version < '3.10' 28 | scikit-image==0.25.2 ; python_full_version >= '3.10' 29 | scipy==1.13.1 ; python_full_version < '3.10' 30 | scipy==1.15.2 ; python_full_version >= '3.10' 31 | tifffile==2024.8.30 ; python_full_version < '3.10' 32 | tifffile==2025.2.18 ; python_full_version >= '3.10' 33 | typing-extensions==4.12.2 ; python_full_version < '3.11' 34 | win32-setctime==1.2.0 ; sys_platform == 'win32' 35 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # LayeredImage Tests 2 | 3 | These are automated tests - Yay! 4 | 5 | Test with 6 | 7 | ```bash 8 | pytest test/test.py 9 | ``` 10 | 11 | Example Output 12 | 13 | ```bash 14 | collected 7 items 15 | 16 | test\test.py ....... [100%] 17 | 18 | =============================================== 7 passed in 24.45s ================================================ 19 | ``` 20 | 21 | Or alert on all warnings 22 | 23 | ```bash 24 | python -Wd test/test.py 25 | ``` 26 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/__init__.py -------------------------------------------------------------------------------- /tests/data/gif_output.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/gif_output.gif -------------------------------------------------------------------------------- /tests/data/gif_output.ora: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/gif_output.ora -------------------------------------------------------------------------------- /tests/data/gif_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/gif_output.png -------------------------------------------------------------------------------- /tests/data/gif_output.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/gif_output.webp -------------------------------------------------------------------------------- /tests/data/gif_output_expected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/gif_output_expected.png -------------------------------------------------------------------------------- /tests/data/layered_image.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layered_image.gif -------------------------------------------------------------------------------- /tests/data/layered_image.layered: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layered_image.layered -------------------------------------------------------------------------------- /tests/data/layered_image.layeredc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layered_image.layeredc -------------------------------------------------------------------------------- /tests/data/layered_image.lsr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layered_image.lsr -------------------------------------------------------------------------------- /tests/data/layered_image.ora: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layered_image.ora -------------------------------------------------------------------------------- /tests/data/layered_image.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layered_image.pdn -------------------------------------------------------------------------------- /tests/data/layered_image.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layered_image.psd -------------------------------------------------------------------------------- /tests/data/layered_image.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layered_image.tiff -------------------------------------------------------------------------------- /tests/data/layered_image.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layered_image.webp -------------------------------------------------------------------------------- /tests/data/layered_image.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layered_image.xcf -------------------------------------------------------------------------------- /tests/data/layered_image_expected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layered_image_expected.png -------------------------------------------------------------------------------- /tests/data/layered_image_expected_nh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layered_image_expected_nh.png -------------------------------------------------------------------------------- /tests/data/layered_output.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layered_output.gif -------------------------------------------------------------------------------- /tests/data/layered_output.ora: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layered_output.ora -------------------------------------------------------------------------------- /tests/data/layered_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layered_output.png -------------------------------------------------------------------------------- /tests/data/layered_output.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layered_output.tiff -------------------------------------------------------------------------------- /tests/data/layered_output.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layered_output.webp -------------------------------------------------------------------------------- /tests/data/layeredc_output.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layeredc_output.gif -------------------------------------------------------------------------------- /tests/data/layeredc_output.ora: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layeredc_output.ora -------------------------------------------------------------------------------- /tests/data/layeredc_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layeredc_output.png -------------------------------------------------------------------------------- /tests/data/layeredc_output.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layeredc_output.tiff -------------------------------------------------------------------------------- /tests/data/layeredc_output.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/layeredc_output.webp -------------------------------------------------------------------------------- /tests/data/lsr_output.lsr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/lsr_output.lsr -------------------------------------------------------------------------------- /tests/data/lsr_output.ora: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/lsr_output.ora -------------------------------------------------------------------------------- /tests/data/lsr_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/lsr_output.png -------------------------------------------------------------------------------- /tests/data/lsr_output.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/lsr_output.webp -------------------------------------------------------------------------------- /tests/data/ora_output.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/ora_output.gif -------------------------------------------------------------------------------- /tests/data/ora_output.layered: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/ora_output.layered -------------------------------------------------------------------------------- /tests/data/ora_output.layeredc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/ora_output.layeredc -------------------------------------------------------------------------------- /tests/data/ora_output.ora: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/ora_output.ora -------------------------------------------------------------------------------- /tests/data/ora_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/ora_output.png -------------------------------------------------------------------------------- /tests/data/ora_output.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/ora_output.tiff -------------------------------------------------------------------------------- /tests/data/ora_output.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/ora_output.webp -------------------------------------------------------------------------------- /tests/data/pdn_output.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/pdn_output.gif -------------------------------------------------------------------------------- /tests/data/pdn_output.ora: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/pdn_output.ora -------------------------------------------------------------------------------- /tests/data/pdn_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/pdn_output.png -------------------------------------------------------------------------------- /tests/data/pdn_output.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/pdn_output.tiff -------------------------------------------------------------------------------- /tests/data/pdn_output.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/pdn_output.webp -------------------------------------------------------------------------------- /tests/data/psd_output.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/psd_output.gif -------------------------------------------------------------------------------- /tests/data/psd_output.ora: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/psd_output.ora -------------------------------------------------------------------------------- /tests/data/psd_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/psd_output.png -------------------------------------------------------------------------------- /tests/data/psd_output.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/psd_output.tiff -------------------------------------------------------------------------------- /tests/data/psd_output.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/psd_output.webp -------------------------------------------------------------------------------- /tests/data/tiff_output.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/tiff_output.gif -------------------------------------------------------------------------------- /tests/data/tiff_output.ora: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/tiff_output.ora -------------------------------------------------------------------------------- /tests/data/tiff_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/tiff_output.png -------------------------------------------------------------------------------- /tests/data/tiff_output.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/tiff_output.tiff -------------------------------------------------------------------------------- /tests/data/tiff_output.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/tiff_output.webp -------------------------------------------------------------------------------- /tests/data/webp_output.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/webp_output.gif -------------------------------------------------------------------------------- /tests/data/webp_output.ora: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/webp_output.ora -------------------------------------------------------------------------------- /tests/data/webp_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/webp_output.png -------------------------------------------------------------------------------- /tests/data/webp_output.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/webp_output.webp -------------------------------------------------------------------------------- /tests/data/xcf_output.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/xcf_output.gif -------------------------------------------------------------------------------- /tests/data/xcf_output.ora: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/xcf_output.ora -------------------------------------------------------------------------------- /tests/data/xcf_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/xcf_output.png -------------------------------------------------------------------------------- /tests/data/xcf_output.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/xcf_output.tiff -------------------------------------------------------------------------------- /tests/data/xcf_output.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/LayeredImage/4fa8e4f08d202cd3db3f8f79732559b8ee679788/tests/data/xcf_output.webp -------------------------------------------------------------------------------- /tests/test_layeredimage.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from PIL import Image 3 | 4 | from layeredimage.layeredimage import LayeredImage 5 | from layeredimage.layergroup import Group, Layer 6 | 7 | 8 | @pytest.fixture 9 | def example_layer(): 10 | # Provide a simple example layer for testing 11 | return Layer(name="Example Layer", image=Image.new("RGBA", (100, 100))) 12 | 13 | 14 | @pytest.fixture 15 | def example_group(example_layer): 16 | # Provide a simple example group containing the example layer for testing 17 | return Group(name="Example Group", layers=[example_layer]) 18 | 19 | 20 | @pytest.fixture 21 | def example_image(example_group): 22 | # Provide a simple example LayeredImage containing the example group for testing 23 | return LayeredImage(layersAndGroups=[example_group]) 24 | 25 | 26 | def test_layered_image_init() -> None: 27 | # Test initialization of LayeredImage 28 | img = LayeredImage([]) 29 | assert isinstance(img, LayeredImage) 30 | 31 | 32 | def test_add_layer_or_group(example_image, example_layer) -> None: 33 | # Test adding a layer to LayeredImage 34 | initial_count = len(example_image.layersAndGroups) 35 | example_image.addLayerOrGroup(example_layer) 36 | assert len(example_image.layersAndGroups) == initial_count + 1 37 | 38 | 39 | def test_insert_layer_or_group(example_image, example_layer) -> None: 40 | # Test inserting a layer into LayeredImage 41 | example_image.insertLayerOrGroup(example_layer, 0) 42 | assert example_image.layersAndGroups[0] == example_layer 43 | 44 | 45 | def test_remove_layer_or_group(example_image, example_layer) -> None: 46 | # Test removing a layer from LayeredImage 47 | example_image.addLayerOrGroup(example_layer) 48 | initial_count = len(example_image.layersAndGroups) 49 | example_image.removeLayerOrGroup(0) 50 | assert len(example_image.layersAndGroups) == initial_count - 1 51 | 52 | 53 | def test_get_layer_or_group(example_image, example_group) -> None: 54 | # Test getting a layer or group from LayeredImage 55 | assert example_image.getLayerOrGroup(0) == example_group 56 | 57 | 58 | def test_extract_layers(example_image, example_layer) -> None: 59 | # Test extracting layers from LayeredImage 60 | example_image.addLayerOrGroup(example_layer) 61 | assert len(example_image.extractLayers()) == 2 62 | 63 | 64 | def test_update_layers(example_image, example_layer) -> None: 65 | # Test updating layers in LayeredImage 66 | example_image.addLayerOrGroup(example_layer) 67 | example_image.updateLayers() 68 | assert len(example_image.layers) == 2 69 | 70 | 71 | def test_extract_groups(example_image, example_group) -> None: 72 | # Test extracting groups from LayeredImage 73 | assert len(example_image.extractGroups()) == 1 74 | 75 | 76 | def test_update_groups(example_image, example_group) -> None: 77 | # Test updating groups in LayeredImage 78 | example_image.addLayerOrGroup(example_group) 79 | example_image.updateGroups() 80 | assert len(example_image.groups) == 2 81 | 82 | 83 | def test_get_flatten_layers(example_image) -> None: 84 | # Test flattening layers in LayeredImage 85 | img = example_image.getFlattenLayers() 86 | assert isinstance(img, Image.Image) 87 | 88 | 89 | def test_layered_image_repr(example_image) -> None: 90 | # Test string representation of LayeredImage 91 | assert repr(example_image) == str(example_image) 92 | 93 | 94 | def test_layered_image_str(example_image) -> None: 95 | # Test string representation of LayeredImage 96 | assert str(example_image) == "" 97 | 98 | 99 | def test_layered_image_json(example_image) -> None: 100 | # Test getting LayeredImage as a dictionary 101 | assert isinstance(example_image.json(), dict) 102 | -------------------------------------------------------------------------------- /tests/test_layergroup.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from PIL import Image 3 | 4 | from layeredimage.layergroup import Group, Layer 5 | 6 | 7 | @pytest.fixture 8 | def example_image(): 9 | # Provide a simple example PIL image for testing 10 | return Image.new("RGBA", (100, 100)) 11 | 12 | 13 | @pytest.fixture 14 | def example_layer(example_image): 15 | # Provide a simple example layer for testing 16 | return Layer(name="Example Layer", image=example_image) 17 | 18 | 19 | @pytest.fixture 20 | def example_group(example_layer): 21 | # Provide a simple example group containing the example layer for testing 22 | return Group(name="Example Group", layers=[example_layer]) 23 | 24 | 25 | def test_layer_group_init(example_image) -> None: 26 | # Test initialization of LayerGroup 27 | layer_group = Layer(name="Test Layer", dimensions=(100, 100), image=example_image) 28 | assert isinstance(layer_group, Layer) 29 | 30 | 31 | def test_layer_init(example_image) -> None: 32 | # Test initialization of Layer 33 | layer = Layer(name="Test Layer", image=example_image) 34 | assert isinstance(layer, Layer) 35 | 36 | 37 | def test_group_init(example_layer) -> None: 38 | # Test initialization of Group 39 | group = Group(name="Test Group", layers=[example_layer]) 40 | assert isinstance(group, Group) 41 | 42 | 43 | def test_layer_group_repr(example_layer) -> None: 44 | # Test string representation of LayerGroup 45 | assert repr(example_layer) == str(example_layer) 46 | 47 | 48 | def test_layer_group_str(example_layer) -> None: 49 | # Test string representation of LayerGroup 50 | assert ( 51 | str(example_layer) 52 | == f'' 53 | ) 54 | 55 | 56 | def test_layer_json(example_layer) -> None: 57 | # Test getting Layer as a dictionary 58 | assert isinstance(example_layer.json(), dict) 59 | 60 | 61 | def test_group_json(example_group) -> None: 62 | # Test getting Group as a dictionary 63 | assert isinstance(example_group.json(), dict) 64 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | """Test module""" 2 | 3 | from __future__ import annotations 4 | 5 | import sys 6 | from pathlib import Path 7 | 8 | from imgcompare import is_equal 9 | 10 | THISDIR = Path(__file__).resolve().parent 11 | sys.path.insert(0, str(THISDIR.parent)) 12 | 13 | import layeredimage.io 14 | 15 | 16 | # ORA 17 | def test_ora() -> None: 18 | ora = layeredimage.io.openLayerImage(f"{THISDIR}/data/layered_image.ora") 19 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/ora_output.ora", ora) 20 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/ora_output.tiff", ora) 21 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/ora_output.webp", ora) 22 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/ora_output.gif", ora) 23 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/ora_output.layered", ora) 24 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/ora_output.layeredc", ora) 25 | ora.getFlattenLayers().save(f"{THISDIR}/data/ora_output.png") 26 | assert is_equal( 27 | f"{THISDIR}/data/ora_output.png", 28 | f"{THISDIR}/data/layered_image_expected.png", 29 | tolerance=0.2, 30 | ) 31 | 32 | 33 | # PSD 34 | def test_psd() -> None: 35 | psd = layeredimage.io.openLayerImage(f"{THISDIR}/data/layered_image.psd") 36 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/psd_output.ora", psd) 37 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/psd_output.tiff", psd) 38 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/psd_output.webp", psd) 39 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/psd_output.gif", psd) 40 | psd.getFlattenLayers().save(f"{THISDIR}/data/psd_output.png") 41 | assert is_equal( 42 | f"{THISDIR}/data/psd_output.png", 43 | f"{THISDIR}/data/layered_image_expected.png", 44 | tolerance=0.2, 45 | ) 46 | 47 | 48 | # PDN 49 | def test_pdn() -> None: 50 | pdn = layeredimage.io.openLayerImage(f"{THISDIR}/data/layered_image.pdn") 51 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/pdn_output.ora", pdn) 52 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/pdn_output.tiff", pdn) 53 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/pdn_output.webp", pdn) 54 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/pdn_output.gif", pdn) 55 | pdn.getFlattenLayers().save(f"{THISDIR}/data/pdn_output.png") 56 | assert is_equal( 57 | f"{THISDIR}/data/pdn_output.png", 58 | f"{THISDIR}/data/layered_image_expected.png", 59 | tolerance=0.2, 60 | ) 61 | 62 | 63 | # XCF 64 | def test_xcf() -> None: 65 | xcf = layeredimage.io.openLayerImage(f"{THISDIR}/data/layered_image.xcf") 66 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/xcf_output.ora", xcf) 67 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/xcf_output.tiff", xcf) 68 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/xcf_output.webp", xcf) 69 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/xcf_output.gif", xcf) 70 | xcf.getFlattenLayers().save(f"{THISDIR}/data/xcf_output.png") 71 | assert is_equal( 72 | f"{THISDIR}/data/xcf_output.png", 73 | f"{THISDIR}/data/layered_image_expected.png", 74 | tolerance=0.2, 75 | ) 76 | 77 | 78 | # TIFF 79 | def test_tiff() -> None: 80 | tiff = layeredimage.io.openLayerImage(f"{THISDIR}/data/layered_image.tiff") 81 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/tiff_output.ora", tiff) 82 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/tiff_output.tiff", tiff) 83 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/tiff_output.webp", tiff) 84 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/tiff_output.gif", tiff) 85 | tiff.getFlattenLayers().save(f"{THISDIR}/data/tiff_output.png") 86 | assert is_equal( 87 | f"{THISDIR}/data/tiff_output.png", 88 | f"{THISDIR}/data/layered_image_expected_nh.png", 89 | tolerance=0.2, 90 | ) 91 | 92 | 93 | # WEBP 94 | def test_webp() -> None: 95 | webp = layeredimage.io.openLayerImage(f"{THISDIR}/data/layered_image.webp") 96 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/webp_output.ora", webp) 97 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/webp_output.webp", webp) 98 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/webp_output.webp", webp) 99 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/webp_output.gif", webp) 100 | webp.getFlattenLayers().save(f"{THISDIR}/data/webp_output.png") 101 | assert is_equal( 102 | f"{THISDIR}/data/webp_output.png", 103 | f"{THISDIR}/data/layered_image_expected_nh.png", 104 | tolerance=0.2, 105 | ) 106 | 107 | 108 | # GIF 109 | def test_gif() -> None: 110 | gif = layeredimage.io.openLayerImage(f"{THISDIR}/data/layered_image.gif") 111 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/gif_output.ora", gif) 112 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/gif_output.gif", gif) 113 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/gif_output.webp", gif) 114 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/gif_output.gif", gif) 115 | gif.getFlattenLayers().save(f"{THISDIR}/data/gif_output.png") 116 | assert is_equal( 117 | f"{THISDIR}/data/gif_output.png", 118 | f"{THISDIR}/data/gif_output_expected.png", 119 | tolerance=0.2, 120 | ) 121 | 122 | 123 | # LSR 124 | def test_lsr() -> None: 125 | lsr = layeredimage.io.openLayerImage(f"{THISDIR}/data/layered_image.lsr") 126 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/lsr_output.ora", lsr) 127 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/lsr_output.lsr", lsr) 128 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/lsr_output.webp", lsr) 129 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/lsr_output.lsr", lsr) 130 | lsr.getFlattenLayers().save(f"{THISDIR}/data/lsr_output.png") 131 | assert is_equal( 132 | f"{THISDIR}/data/lsr_output.png", 133 | f"{THISDIR}/data/layered_image_expected_nh.png", 134 | tolerance=0.2, 135 | ) 136 | 137 | 138 | # LAYERED 139 | def test_layered() -> None: 140 | layered = layeredimage.io.openLayerImage(f"{THISDIR}/data/layered_image.layered") 141 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/layered_output.ora", layered) 142 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/layered_output.tiff", layered) 143 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/layered_output.webp", layered) 144 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/layered_output.gif", layered) 145 | layered.getFlattenLayers().save(f"{THISDIR}/data/layered_output.png") 146 | assert is_equal( 147 | f"{THISDIR}/data/layered_output.png", 148 | f"{THISDIR}/data/layered_image_expected.png", 149 | tolerance=0.2, 150 | ) 151 | 152 | 153 | # LAYEREDC 154 | def test_layeredc() -> None: 155 | layeredc = layeredimage.io.openLayerImage(f"{THISDIR}/data/layered_image.layeredc") 156 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/layeredc_output.ora", layeredc) 157 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/layeredc_output.tiff", layeredc) 158 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/layeredc_output.webp", layeredc) 159 | layeredimage.io.saveLayerImage(f"{THISDIR}/data/layeredc_output.gif", layeredc) 160 | layeredc.getFlattenLayers().save(f"{THISDIR}/data/layeredc_output.png") 161 | assert is_equal( 162 | f"{THISDIR}/data/layeredc_output.png", 163 | f"{THISDIR}/data/layered_image_expected.png", 164 | tolerance=0.2, 165 | ) 166 | 167 | 168 | if __name__ == "__main__": 169 | import sys 170 | 171 | import pytest 172 | 173 | pytest.main(sys.argv) 174 | --------------------------------------------------------------------------------