├── .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 | [](../../)
2 | [](../../issues)
3 | [](/LICENSE.md)
4 | [](../../commits/master)
5 | [](../../commits/master)
6 | [](https://pypistats.org/packages/layeredimage)
7 | [](https://pepy.tech/project/layeredimage)
8 | [](https://pypi.org/project/layeredimage)
9 |
10 |
11 | # LayeredImage
12 |
13 |
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 |
--------------------------------------------------------------------------------