psdparse 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- psdparse-0.1.0/.gitignore +24 -0
- psdparse-0.1.0/CMakeLists.txt +24 -0
- psdparse-0.1.0/CMakePresets.json +67 -0
- psdparse-0.1.0/LICENSE +21 -0
- psdparse-0.1.0/Makefile +57 -0
- psdparse-0.1.0/PKG-INFO +166 -0
- psdparse-0.1.0/README.md +125 -0
- psdparse-0.1.0/docs/ARCHITECTURE.md +156 -0
- psdparse-0.1.0/docs/PYTHON_API.md +204 -0
- psdparse-0.1.0/docs/ROADMAP.md +78 -0
- psdparse-0.1.0/psdparse/CMakeLists.txt +64 -0
- psdparse-0.1.0/psdparse/bmp.cpp +71 -0
- psdparse-0.1.0/psdparse/psd_cli.cpp +81 -0
- psdparse-0.1.0/psdparse/psdbase.h +149 -0
- psdparse-0.1.0/psdparse/psddata.h +519 -0
- psdparse-0.1.0/psdparse/psddesc.cpp +262 -0
- psdparse-0.1.0/psdparse/psddesc.h +436 -0
- psdparse-0.1.0/psdparse/psdfile.cpp +231 -0
- psdparse-0.1.0/psdparse/psdfile.h +79 -0
- psdparse-0.1.0/psdparse/psdimage.cpp +1134 -0
- psdparse-0.1.0/psdparse/psdlayer.cpp +116 -0
- psdparse-0.1.0/psdparse/psdlayer.h +13 -0
- psdparse-0.1.0/psdparse/psdparse.cpp +589 -0
- psdparse-0.1.0/psdparse/psdparse.h +327 -0
- psdparse-0.1.0/psdparse/psdresource.cpp +164 -0
- psdparse-0.1.0/psdparse/psdresource.h +12 -0
- psdparse-0.1.0/psdparse/psdwrite.cpp +223 -0
- psdparse-0.1.0/psdparse/psdwrite.h +95 -0
- psdparse-0.1.0/pyproject.toml +49 -0
- psdparse-0.1.0/python/CMakeLists.txt +47 -0
- psdparse-0.1.0/python/psdparse_module.cpp +209 -0
- psdparse-0.1.0/tests/conftest.py +67 -0
- psdparse-0.1.0/tests/test_header.py +30 -0
- psdparse-0.1.0/tests/test_images.py +102 -0
- psdparse-0.1.0/tests/test_layers.py +48 -0
- psdparse-0.1.0/tests/test_save.py +83 -0
- psdparse-0.1.0/tools/psd_export.py +123 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/build
|
|
2
|
+
/.vs
|
|
3
|
+
/.vscode
|
|
4
|
+
|
|
5
|
+
# sample PSDs for testing (see README for sourcing)
|
|
6
|
+
*.psd
|
|
7
|
+
|
|
8
|
+
# tools/psd_export.py outputs
|
|
9
|
+
/_export_*/
|
|
10
|
+
|
|
11
|
+
# Python cache and build artifacts
|
|
12
|
+
__pycache__/
|
|
13
|
+
*.pyc
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
*.pyd
|
|
16
|
+
|
|
17
|
+
# install output
|
|
18
|
+
/install/
|
|
19
|
+
|
|
20
|
+
# Python build / packaging artifacts (scikit-build-core, wheels, sdists)
|
|
21
|
+
/dist/
|
|
22
|
+
*.egg-info/
|
|
23
|
+
_wt/
|
|
24
|
+
_skbuild/
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.16)
|
|
2
|
+
|
|
3
|
+
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
|
4
|
+
add_compile_options("$<$<AND:$<C_COMPILER_ID:MSVC>,$<COMPILE_LANGUAGE:C>>:/utf-8>")
|
|
5
|
+
add_compile_options("$<$<AND:$<CXX_COMPILER_ID:MSVC>,$<COMPILE_LANGUAGE:CXX>>:/utf-8>")
|
|
6
|
+
add_compile_options("$<$<AND:$<CXX_COMPILER_ID:MSVC>,$<COMPILE_LANGUAGE:CXX>>:/Zc:__cplusplus>")
|
|
7
|
+
project(psdparse LANGUAGES CXX)
|
|
8
|
+
endif()
|
|
9
|
+
|
|
10
|
+
# Python バインディング (オプション、別プリセット / pip で ON)
|
|
11
|
+
option(PSDPARSE_BUILD_PYTHON "Build pybind11 Python bindings" OFF)
|
|
12
|
+
if(PSDPARSE_BUILD_PYTHON)
|
|
13
|
+
# 拡張モジュールは libpsdparse.a (および取得した zlib) を .so にリンクするため、
|
|
14
|
+
# 静的オブジェクトはすべて位置独立コード (PIC) でビルドする必要がある。
|
|
15
|
+
# add_subdirectory(psdparse) より前に設定し、配下の全ターゲットへ波及させる。
|
|
16
|
+
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
|
17
|
+
endif()
|
|
18
|
+
|
|
19
|
+
# C++ ライブラリ本体 (吉里吉里非依存、pure C++17)
|
|
20
|
+
add_subdirectory(psdparse)
|
|
21
|
+
|
|
22
|
+
if(PSDPARSE_BUILD_PYTHON)
|
|
23
|
+
add_subdirectory(python)
|
|
24
|
+
endif()
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 2,
|
|
3
|
+
"configurePresets": [
|
|
4
|
+
{
|
|
5
|
+
"name": "default",
|
|
6
|
+
"description": "Default build using Ninja Multi-Config (vcpkg-free; zlib via system or FetchContent)",
|
|
7
|
+
"generator": "Ninja Multi-Config",
|
|
8
|
+
"binaryDir": "$env{BUILD_DIR}",
|
|
9
|
+
"environment": {
|
|
10
|
+
"BUILD_DIR": "build/${presetName}"
|
|
11
|
+
},
|
|
12
|
+
"cacheVariables": {
|
|
13
|
+
"CMAKE_CXX_STANDARD": "17",
|
|
14
|
+
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
|
|
15
|
+
"PSDPARSE_BUILD_CLI": "ON"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "x64-windows",
|
|
20
|
+
"description": "Windows x64 (MSVC, static CRT). C++ library + CLI only.",
|
|
21
|
+
"inherits": "default",
|
|
22
|
+
"architecture": {
|
|
23
|
+
"value": "x64",
|
|
24
|
+
"strategy": "external"
|
|
25
|
+
},
|
|
26
|
+
"cacheVariables": {
|
|
27
|
+
"CMAKE_C_COMPILER": "cl",
|
|
28
|
+
"CMAKE_CXX_COMPILER": "cl",
|
|
29
|
+
"CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreaded$<$<CONFIG:Debug>:Debug>"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "x86-windows",
|
|
34
|
+
"description": "Windows x86 (MSVC, static CRT). C++ library + CLI only.",
|
|
35
|
+
"inherits": "default",
|
|
36
|
+
"architecture": {
|
|
37
|
+
"value": "x86",
|
|
38
|
+
"strategy": "external"
|
|
39
|
+
},
|
|
40
|
+
"cacheVariables": {
|
|
41
|
+
"CMAKE_C_COMPILER": "cl",
|
|
42
|
+
"CMAKE_CXX_COMPILER": "cl",
|
|
43
|
+
"CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreaded$<$<CONFIG:Debug>:Debug>"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "x64-windows-python",
|
|
48
|
+
"description": "Windows x64 Python bindings (MSVC, dynamic CRT to match CPython). Prefer `pip install .` (scikit-build-core).",
|
|
49
|
+
"inherits": "default",
|
|
50
|
+
"architecture": {
|
|
51
|
+
"value": "x64",
|
|
52
|
+
"strategy": "external"
|
|
53
|
+
},
|
|
54
|
+
"cacheVariables": {
|
|
55
|
+
"CMAKE_C_COMPILER": "cl",
|
|
56
|
+
"CMAKE_CXX_COMPILER": "cl",
|
|
57
|
+
"CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL",
|
|
58
|
+
"PSDPARSE_BUILD_PYTHON": "ON"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"buildPresets": [
|
|
63
|
+
{ "name": "x64-windows", "configurePreset": "x64-windows" },
|
|
64
|
+
{ "name": "x86-windows", "configurePreset": "x86-windows" },
|
|
65
|
+
{ "name": "x64-windows-python", "configurePreset": "x64-windows-python" }
|
|
66
|
+
]
|
|
67
|
+
}
|
psdparse-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wamsoft
|
|
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.
|
psdparse-0.1.0/Makefile
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
SHELL = /bin/bash
|
|
2
|
+
|
|
3
|
+
# vcpkg-free: zlib comes from the system or is fetched from source by CMake.
|
|
4
|
+
|
|
5
|
+
ifeq ($(shell type cygpath >& /dev/null && echo true),true)
|
|
6
|
+
FIXPATH = cygpath -ma
|
|
7
|
+
else
|
|
8
|
+
FIXPATH = realpath
|
|
9
|
+
endif
|
|
10
|
+
|
|
11
|
+
# Detect OS and set default PRESET accordingly
|
|
12
|
+
ifeq ($(OS),Windows_NT)
|
|
13
|
+
PRESET?=x64-windows
|
|
14
|
+
else
|
|
15
|
+
UNAME_S := $(shell uname -s)
|
|
16
|
+
UNAME_M := $(shell uname -m)
|
|
17
|
+
ifeq ($(UNAME_S),Linux)
|
|
18
|
+
ifeq ($(UNAME_M),aarch64)
|
|
19
|
+
PRESET?=arm64-linux
|
|
20
|
+
else
|
|
21
|
+
PRESET?=x64-linux
|
|
22
|
+
endif
|
|
23
|
+
else ifeq ($(UNAME_S),Darwin)
|
|
24
|
+
ifeq ($(UNAME_M),arm64)
|
|
25
|
+
PRESET?=arm64-osx
|
|
26
|
+
else
|
|
27
|
+
PRESET?=x64-osx
|
|
28
|
+
endif
|
|
29
|
+
else
|
|
30
|
+
PRESET?=x64-windows
|
|
31
|
+
endif
|
|
32
|
+
endif
|
|
33
|
+
|
|
34
|
+
BUILD_TYPE?=Release
|
|
35
|
+
CMAKEOPT?=""
|
|
36
|
+
INSTALL_PREFIX?=install
|
|
37
|
+
|
|
38
|
+
BUILD_PATH=$(shell cmake --preset $(PRESET) -N | grep BUILD_DIR | sed 's/.*BUILD_DIR="\(.*\)"/\1/')
|
|
39
|
+
|
|
40
|
+
.PHONY: prebuild build clean install run
|
|
41
|
+
|
|
42
|
+
all: build
|
|
43
|
+
|
|
44
|
+
# cmake 処理実行
|
|
45
|
+
# CMAKEOPT で引数定義追加
|
|
46
|
+
prebuild:
|
|
47
|
+
cmake --preset $(PRESET) ${CMAKEOPT}
|
|
48
|
+
# ビルド実行
|
|
49
|
+
build:
|
|
50
|
+
cmake --build $(BUILD_PATH) --config $(BUILD_TYPE)
|
|
51
|
+
|
|
52
|
+
clean:
|
|
53
|
+
cmake --build $(BUILD_PATH) --config $(BUILD_TYPE) --target clean
|
|
54
|
+
|
|
55
|
+
install:
|
|
56
|
+
cmake --install $(BUILD_PATH) --config $(BUILD_TYPE) --prefix $(INSTALL_PREFIX)
|
|
57
|
+
|
psdparse-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: psdparse
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fast PSD (Photoshop) reader/writer — C++17 core with pybind11 bindings
|
|
5
|
+
Keywords: psd,photoshop,parser,image,graphics
|
|
6
|
+
Author-Email: wamsoft <wtnbgo@gmail.com>
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 wamsoft
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
|
|
29
|
+
Classifier: Development Status :: 4 - Beta
|
|
30
|
+
Classifier: Intended Audience :: Developers
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Programming Language :: C++
|
|
33
|
+
Classifier: Programming Language :: Python :: 3
|
|
34
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
35
|
+
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
|
|
36
|
+
Project-URL: Homepage, https://github.com/wamsoft/psdparse
|
|
37
|
+
Project-URL: Repository, https://github.com/wamsoft/psdparse
|
|
38
|
+
Project-URL: Issues, https://github.com/wamsoft/psdparse/issues
|
|
39
|
+
Requires-Python: >=3.9
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# psdparse
|
|
43
|
+
|
|
44
|
+
Pure C++17 PSD (Photoshop) reader/writer library, with pybind11-based Python bindings.
|
|
45
|
+
|
|
46
|
+
- Lazy I/O: PSD pixel data is **not** copied into memory at parse time. Only the structural metadata (a few hundred KB even for large files) is read upfront; layer pixels are paged in on demand via mmap or stream callbacks.
|
|
47
|
+
- Round-trip save: `load(p) -> save(q)` produces a byte-identical PSD file.
|
|
48
|
+
- Python wrapper: `import psdparse` → `PSDFile.load(path) / layer_image(i) / merged_image() / save(path)`.
|
|
49
|
+
|
|
50
|
+
The library was extracted from the [psdfile](https://github.com/wamsoft/psdfile) kirikiri plugin in 2026. psdfile now consumes this library as a submodule.
|
|
51
|
+
|
|
52
|
+
## Architecture (quick read)
|
|
53
|
+
|
|
54
|
+
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for full details.
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
IteratorBase (psdbase.h, pure virtual — parser only sees this)
|
|
58
|
+
├── MemoryReader (psdparse.h) ... mmap / contiguous buffer
|
|
59
|
+
└── StreamReader (psdparse.h) ... arbitrary seekable stream
|
|
60
|
+
└── Source (pure virtual; subclass per backend)
|
|
61
|
+
├── IStreamSource ... std::istream
|
|
62
|
+
└── (your custom Source) ... e.g. iTJSBinaryStream wrapper
|
|
63
|
+
|
|
64
|
+
WriterBase (psdwrite.h, pure virtual — symmetric to IteratorBase)
|
|
65
|
+
└── FileWriter (FILE*)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`psd::PSDFile::load(const char *path)` mmaps a local file. `loadFromStream(std::istream&)` / `loadFromReader(IteratorBase&)` accept arbitrary I/O. `save(const char *path)` writes the loaded data back as PSD.
|
|
69
|
+
|
|
70
|
+
All public path arguments are **UTF-8** (`char *`). On Win32, conversion to UTF-16 happens internally via `psd::utf8ToWide` (inline in psdbase.h).
|
|
71
|
+
|
|
72
|
+
## Install (Python)
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install psdparse # once published to PyPI
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Build from source — needs only a C++17 compiler + CMake 3.16+, **no vcpkg**:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install . # or: pip wheel . -w dist
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`zlib` is taken from the system if present, otherwise fetched from source by
|
|
85
|
+
CMake (`FetchContent`), so no package manager is required. Packaging uses
|
|
86
|
+
[scikit-build-core](https://scikit-build-core.readthedocs.io/); cross-platform
|
|
87
|
+
wheels are built in CI (`.github/workflows/wheels.yml`, cibuildwheel).
|
|
88
|
+
|
|
89
|
+
## Build (C++ library / CLI)
|
|
90
|
+
|
|
91
|
+
Requires CMake 3.16+ and a C++17 compiler. **vcpkg is no longer needed.**
|
|
92
|
+
|
|
93
|
+
```powershell
|
|
94
|
+
# C++ library + CLI (static CRT)
|
|
95
|
+
cmake --preset x64-windows
|
|
96
|
+
cmake --build --preset x64-windows --config Release
|
|
97
|
+
|
|
98
|
+
# C++ library + Python module (dynamic CRT, matches CPython)
|
|
99
|
+
cmake --preset x64-windows-python
|
|
100
|
+
cmake --build --preset x64-windows-python --config Release
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`Makefile` is a thin wrapper:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
make PRESET=x64-windows prebuild build
|
|
107
|
+
make PRESET=x64-windows-python prebuild build
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Build artifacts:
|
|
111
|
+
- `build/x64-windows/psdparse/Release/psdparse_cli.exe`
|
|
112
|
+
- `build/x64-windows-python/python/Release/psdparse.cp312-win_amd64.pyd`
|
|
113
|
+
|
|
114
|
+
Only dependency: `zlib` (system, or auto-fetched from source).
|
|
115
|
+
|
|
116
|
+
## Python API
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import psdparse
|
|
120
|
+
|
|
121
|
+
p = psdparse.PSDFile()
|
|
122
|
+
p.load(r"path/to/file.psd") # mmap-backed
|
|
123
|
+
|
|
124
|
+
print(p.header.width, p.header.height, len(p.layers))
|
|
125
|
+
for layer in p.layers:
|
|
126
|
+
print(layer.name_unicode, layer.blend_mode.name, layer.opacity)
|
|
127
|
+
|
|
128
|
+
bgra = p.merged_image() # bytes, BGRA, 4*W*H
|
|
129
|
+
layer_bgra = p.layer_image(0, "masked") # bytes, BGRA, 4*w*h
|
|
130
|
+
|
|
131
|
+
p.save(r"out.psd") # byte-identical round-trip
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Full API reference: [docs/PYTHON_API.md](docs/PYTHON_API.md).
|
|
135
|
+
|
|
136
|
+
## Tests
|
|
137
|
+
|
|
138
|
+
Tests live under `tests/` and use [pytest](https://docs.pytest.org/). They need two sample PSDs at the repo root (not in git — see below).
|
|
139
|
+
|
|
140
|
+
```powershell
|
|
141
|
+
# After building with x64-windows-python preset:
|
|
142
|
+
python -m pytest -v
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Sample PSDs
|
|
146
|
+
|
|
147
|
+
The 27 pytest tests use these files:
|
|
148
|
+
|
|
149
|
+
| File | Size | Description |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| `UI-PSDサンプル.psd` | 800×600, 50 layers, ~2 MB | UI button mock-up. Folder groups, transparent overlays. |
|
|
152
|
+
| `園部由夏_a.psd` | 2500×3500, 28 layers, ~21 MB | Illustration. PASS_THROUGH groups, Unicode layer names (luni records). |
|
|
153
|
+
|
|
154
|
+
Place them at the repo root (the conftest.py also checks `tests/data/` first). They are listed in `.gitignore`. If you can't source them, the tests will skip rather than fail.
|
|
155
|
+
|
|
156
|
+
## tools/psd_export.py
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
python tools/psd_export.py input.psd [--out-dir DIR] [--mode masked|image|mask]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Outputs `layers.json` (full layer metadata), `merged.png` (composite), and per-layer PNGs.
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
MIT — see [LICENSE](LICENSE).
|
psdparse-0.1.0/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# psdparse
|
|
2
|
+
|
|
3
|
+
Pure C++17 PSD (Photoshop) reader/writer library, with pybind11-based Python bindings.
|
|
4
|
+
|
|
5
|
+
- Lazy I/O: PSD pixel data is **not** copied into memory at parse time. Only the structural metadata (a few hundred KB even for large files) is read upfront; layer pixels are paged in on demand via mmap or stream callbacks.
|
|
6
|
+
- Round-trip save: `load(p) -> save(q)` produces a byte-identical PSD file.
|
|
7
|
+
- Python wrapper: `import psdparse` → `PSDFile.load(path) / layer_image(i) / merged_image() / save(path)`.
|
|
8
|
+
|
|
9
|
+
The library was extracted from the [psdfile](https://github.com/wamsoft/psdfile) kirikiri plugin in 2026. psdfile now consumes this library as a submodule.
|
|
10
|
+
|
|
11
|
+
## Architecture (quick read)
|
|
12
|
+
|
|
13
|
+
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for full details.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
IteratorBase (psdbase.h, pure virtual — parser only sees this)
|
|
17
|
+
├── MemoryReader (psdparse.h) ... mmap / contiguous buffer
|
|
18
|
+
└── StreamReader (psdparse.h) ... arbitrary seekable stream
|
|
19
|
+
└── Source (pure virtual; subclass per backend)
|
|
20
|
+
├── IStreamSource ... std::istream
|
|
21
|
+
└── (your custom Source) ... e.g. iTJSBinaryStream wrapper
|
|
22
|
+
|
|
23
|
+
WriterBase (psdwrite.h, pure virtual — symmetric to IteratorBase)
|
|
24
|
+
└── FileWriter (FILE*)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`psd::PSDFile::load(const char *path)` mmaps a local file. `loadFromStream(std::istream&)` / `loadFromReader(IteratorBase&)` accept arbitrary I/O. `save(const char *path)` writes the loaded data back as PSD.
|
|
28
|
+
|
|
29
|
+
All public path arguments are **UTF-8** (`char *`). On Win32, conversion to UTF-16 happens internally via `psd::utf8ToWide` (inline in psdbase.h).
|
|
30
|
+
|
|
31
|
+
## Install (Python)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install psdparse # once published to PyPI
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Build from source — needs only a C++17 compiler + CMake 3.16+, **no vcpkg**:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install . # or: pip wheel . -w dist
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`zlib` is taken from the system if present, otherwise fetched from source by
|
|
44
|
+
CMake (`FetchContent`), so no package manager is required. Packaging uses
|
|
45
|
+
[scikit-build-core](https://scikit-build-core.readthedocs.io/); cross-platform
|
|
46
|
+
wheels are built in CI (`.github/workflows/wheels.yml`, cibuildwheel).
|
|
47
|
+
|
|
48
|
+
## Build (C++ library / CLI)
|
|
49
|
+
|
|
50
|
+
Requires CMake 3.16+ and a C++17 compiler. **vcpkg is no longer needed.**
|
|
51
|
+
|
|
52
|
+
```powershell
|
|
53
|
+
# C++ library + CLI (static CRT)
|
|
54
|
+
cmake --preset x64-windows
|
|
55
|
+
cmake --build --preset x64-windows --config Release
|
|
56
|
+
|
|
57
|
+
# C++ library + Python module (dynamic CRT, matches CPython)
|
|
58
|
+
cmake --preset x64-windows-python
|
|
59
|
+
cmake --build --preset x64-windows-python --config Release
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`Makefile` is a thin wrapper:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
make PRESET=x64-windows prebuild build
|
|
66
|
+
make PRESET=x64-windows-python prebuild build
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Build artifacts:
|
|
70
|
+
- `build/x64-windows/psdparse/Release/psdparse_cli.exe`
|
|
71
|
+
- `build/x64-windows-python/python/Release/psdparse.cp312-win_amd64.pyd`
|
|
72
|
+
|
|
73
|
+
Only dependency: `zlib` (system, or auto-fetched from source).
|
|
74
|
+
|
|
75
|
+
## Python API
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import psdparse
|
|
79
|
+
|
|
80
|
+
p = psdparse.PSDFile()
|
|
81
|
+
p.load(r"path/to/file.psd") # mmap-backed
|
|
82
|
+
|
|
83
|
+
print(p.header.width, p.header.height, len(p.layers))
|
|
84
|
+
for layer in p.layers:
|
|
85
|
+
print(layer.name_unicode, layer.blend_mode.name, layer.opacity)
|
|
86
|
+
|
|
87
|
+
bgra = p.merged_image() # bytes, BGRA, 4*W*H
|
|
88
|
+
layer_bgra = p.layer_image(0, "masked") # bytes, BGRA, 4*w*h
|
|
89
|
+
|
|
90
|
+
p.save(r"out.psd") # byte-identical round-trip
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Full API reference: [docs/PYTHON_API.md](docs/PYTHON_API.md).
|
|
94
|
+
|
|
95
|
+
## Tests
|
|
96
|
+
|
|
97
|
+
Tests live under `tests/` and use [pytest](https://docs.pytest.org/). They need two sample PSDs at the repo root (not in git — see below).
|
|
98
|
+
|
|
99
|
+
```powershell
|
|
100
|
+
# After building with x64-windows-python preset:
|
|
101
|
+
python -m pytest -v
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Sample PSDs
|
|
105
|
+
|
|
106
|
+
The 27 pytest tests use these files:
|
|
107
|
+
|
|
108
|
+
| File | Size | Description |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `UI-PSDサンプル.psd` | 800×600, 50 layers, ~2 MB | UI button mock-up. Folder groups, transparent overlays. |
|
|
111
|
+
| `園部由夏_a.psd` | 2500×3500, 28 layers, ~21 MB | Illustration. PASS_THROUGH groups, Unicode layer names (luni records). |
|
|
112
|
+
|
|
113
|
+
Place them at the repo root (the conftest.py also checks `tests/data/` first). They are listed in `.gitignore`. If you can't source them, the tests will skip rather than fail.
|
|
114
|
+
|
|
115
|
+
## tools/psd_export.py
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
python tools/psd_export.py input.psd [--out-dir DIR] [--mode masked|image|mask]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Outputs `layers.json` (full layer metadata), `merged.png` (composite), and per-layer PNGs.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# psdparse Architecture
|
|
2
|
+
|
|
3
|
+
## Goals (in priority order)
|
|
4
|
+
|
|
5
|
+
1. **Lazy I/O.** Loading a PSD must not require reading the full file into memory. Only structural metadata (a few hundred KB) is touched during parse. Pixel data is paged in when the user asks for a specific layer.
|
|
6
|
+
2. **Backend-agnostic.** The parser and the image decoder see one I/O abstraction (`IteratorBase`). mmap and arbitrary seekable streams (std::istream, kirikiri `iTJSBinaryStream`, …) all plug into the same code path.
|
|
7
|
+
3. **Round-trip save.** `load(p) -> save(q)` produces a byte-identical PSD. We achieve this by retaining raw-bytes iterators for blocks whose round-trip re-serialization would be painful (layer extra data, global mask info, trailing additional info).
|
|
8
|
+
4. **No Boost. No tp_stub.** Plain C++17 + zlib only.
|
|
9
|
+
|
|
10
|
+
## Read path
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
IteratorBase (psdbase.h, pure virtual)
|
|
14
|
+
├── MemoryReader (psdparse.h)
|
|
15
|
+
│ base_, [start_, end_), pos_ ← all just offsets into a const uint8_t*
|
|
16
|
+
│ Used for: mmap'd files (PSDFile::load) and contiguous buffers (loadFromMemory)
|
|
17
|
+
│
|
|
18
|
+
└── StreamReader (psdparse.h)
|
|
19
|
+
shared_ptr<Source> + 4KB cache + [start_, end_), pos_
|
|
20
|
+
Used for: any seekable stream
|
|
21
|
+
└── Source (pure virtual, nested in StreamReader)
|
|
22
|
+
├── IStreamSource ... std::istream wrapper (psdfile.cpp)
|
|
23
|
+
└── (your backend) ... e.g. iTJSBinaryStream wrapper
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`IteratorBase` exposes a tiny interface:
|
|
27
|
+
|
|
28
|
+
```cpp
|
|
29
|
+
virtual int getCh();
|
|
30
|
+
virtual int getData(void *buffer, int n);
|
|
31
|
+
virtual int16_t getInt16(bool convToNative=true);
|
|
32
|
+
virtual int32_t getInt32(bool convToNative=true);
|
|
33
|
+
virtual int64_t getInt64(bool convToNative=true);
|
|
34
|
+
virtual void getUnicodeString(u16str &out, bool convToNative=true);
|
|
35
|
+
|
|
36
|
+
virtual void advance(int n);
|
|
37
|
+
virtual void init(); // reset to start of this sub-range
|
|
38
|
+
virtual int size(); // range length
|
|
39
|
+
virtual int rest(); // remaining bytes from pos
|
|
40
|
+
virtual bool eoi();
|
|
41
|
+
|
|
42
|
+
virtual IteratorBase *clone(); // same pos, same range
|
|
43
|
+
virtual IteratorBase *cloneOffset(int offset); // pos+offset, end inherited
|
|
44
|
+
virtual IteratorBase *cloneRange(int offset, int len); // [pos+offset, pos+offset+len)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The parser only ever sees `IteratorBase &`. There is no virtual function for "switch backend" because the choice of backend is fixed at construction time and forwarded via the `clone*` operations.
|
|
48
|
+
|
|
49
|
+
### `cloneRange` is load-bearing
|
|
50
|
+
|
|
51
|
+
Size-prefixed blocks (image resource entries, layer extra data, global mask info, …) must be parsed inside an `IteratorBase` that is **strictly bounded** to the declared size. Otherwise a corrupt `dataSize` can drive the parser past block boundaries — in the worst case, into an infinite `push_back` loop that allocates gigabytes before being noticed.
|
|
52
|
+
|
|
53
|
+
The `SubBlock` RAII helper in `psdparse.cpp` wraps this pattern:
|
|
54
|
+
|
|
55
|
+
```cpp
|
|
56
|
+
SubBlock blk(outerReader, declaredSize); // bounds sub-reader to declaredSize
|
|
57
|
+
parseInnerStuff(blk.reader()); // sub-parser cannot escape
|
|
58
|
+
// dtor: outerReader.advance(declaredSize) regardless of what sub consumed
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Forward-progress guards
|
|
62
|
+
|
|
63
|
+
`parseImageResources`, `parseLayerExtraData`, and similar loops must verify that each iteration advances the reader:
|
|
64
|
+
|
|
65
|
+
```cpp
|
|
66
|
+
while (r.rest() >= MIN_ENTRY) {
|
|
67
|
+
int posBefore = r.size() - r.rest();
|
|
68
|
+
if (!parseOneEntry(r, ...)) break;
|
|
69
|
+
int posAfter = r.size() - r.rest();
|
|
70
|
+
if (posAfter <= posBefore) break; // garbage entry; bail
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This is the second line of defense for corrupt input.
|
|
75
|
+
|
|
76
|
+
## Lifetime: who owns the bytes?
|
|
77
|
+
|
|
78
|
+
The parser stores `IteratorBase*` clones into `psd::Data` fields (`channelImageData`, `imageData`, per-resource `data`, per-layer-channel `imageData`, …). Each clone:
|
|
79
|
+
|
|
80
|
+
- For `MemoryReader`: holds a `const uint8_t*` into the underlying buffer (mmap or `ownedBuffer_`). The buffer is owned by `PSDFile` via `mapping_` or `ownedBuffer_`.
|
|
81
|
+
- For `StreamReader`: holds a `std::shared_ptr<Source>`. Last clone dies → `Source` dies → backend stream is closed.
|
|
82
|
+
|
|
83
|
+
`PSDFile::clearData()` drops the iterators in well-defined order. The mmap is unmapped, owned buffer is swapped to empty, owned `istream` is reset. The destructor chains through `~Data` → `clearData()` (virtual; the explicit call in `~PSD()` is intentional and **must not** be removed).
|
|
84
|
+
|
|
85
|
+
## Write path
|
|
86
|
+
|
|
87
|
+
`writePSD(WriterBase&, const Data&)` walks the same five PSD top-level sections:
|
|
88
|
+
|
|
89
|
+
1. **File header** (26 bytes): re-serialized from `data.header` fields.
|
|
90
|
+
2. **Color mode data**: copies `data.colorModeIterator` bytes back.
|
|
91
|
+
3. **Image resources**: re-emits each resource's 8BIM header / Pascal name / size, then dumps `res.data` iterator bytes.
|
|
92
|
+
4. **Layer and mask info**: re-emits the layer info subsection header + per-record metadata, then dumps `data.channelImageData` (all channel data for all layers, in one blob), then dumps `data.globalLayerMaskInfoRaw` and `data.layerAndMaskTrailing` raw bytes for high-fidelity.
|
|
93
|
+
5. **Image data**: dumps `data.imageData` (composite image, including compression word).
|
|
94
|
+
|
|
95
|
+
### patch-back size headers
|
|
96
|
+
|
|
97
|
+
Variable-length sections start with a 4-byte size field. The writer uses a placeholder-then-seek-back pattern:
|
|
98
|
+
|
|
99
|
+
```cpp
|
|
100
|
+
int64_t sizePos = w.tell();
|
|
101
|
+
w.putUint32BE(0); // placeholder
|
|
102
|
+
int64_t bodyStart = w.tell();
|
|
103
|
+
writeBody(w);
|
|
104
|
+
int64_t bodyEnd = w.tell();
|
|
105
|
+
w.seek(sizePos); // back-patch
|
|
106
|
+
w.putUint32BE(bodyEnd - bodyStart);
|
|
107
|
+
w.seek(bodyEnd);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`WriterBase` requires `tell()` and `seek()` (pure virtual). `FileWriter` implements them with `_ftelli64` / `_fseeki64`.
|
|
111
|
+
|
|
112
|
+
### Raw-bytes iterators for round-trip fidelity
|
|
113
|
+
|
|
114
|
+
Re-serializing the full extra-data block (layer mask: 0 / 20 / 36 / 40 bytes with conditional fields; layer blending ranges; Pascal name with 4-byte padding; nested additional layer info entries) is tedious and error-prone. Instead, the parser captures the entire extra-data block as a raw IteratorBase clone alongside the parsed fields:
|
|
115
|
+
|
|
116
|
+
```cpp
|
|
117
|
+
// psdparse.cpp parseLayerRecord:
|
|
118
|
+
uint32_t extraSize = r.getInt32(true);
|
|
119
|
+
if (extraSize > 0) lay.extraData.rawBytes = r.cloneRange(0, extraSize);
|
|
120
|
+
SubBlock blk(r, extraSize);
|
|
121
|
+
if (extraSize > 0) parseLayerExtraData(blk.reader(), lay.extraData);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`writeLayerRecord` dumps `lay.extraData.rawBytes` directly. The parsed `LayerMask`, `LayerBlendingRange`, etc. fields are used for read access (`getLayerImage` etc.) but ignored on save.
|
|
125
|
+
|
|
126
|
+
Same trick for `Data::globalLayerMaskInfoRaw` and `Data::layerAndMaskTrailing`. The latter is the unparsed Lr16/Lr32 secondary layer info — capturing it added 19KB of fidelity to one of the test fixtures.
|
|
127
|
+
|
|
128
|
+
### Why this matters
|
|
129
|
+
|
|
130
|
+
The round-trip guarantee assumes the user didn't modify the loaded data. Adding/removing layers or changing names breaks it because:
|
|
131
|
+
|
|
132
|
+
- `channelImageData` is the **concatenation** of all channel bytes for all layers. Deleting a `LayerInfo` from `layerList` doesn't shrink this blob.
|
|
133
|
+
- `LayerExtraData::rawBytes` doesn't reflect a mutated layer name. Save would write stale bytes.
|
|
134
|
+
|
|
135
|
+
The roadmap for supporting structural edits is in [ROADMAP.md](ROADMAP.md). Briefly: per-channel save, then field-based extra-data emission, then an RLE encoder for new pixel data.
|
|
136
|
+
|
|
137
|
+
## File / class index
|
|
138
|
+
|
|
139
|
+
| File | Role |
|
|
140
|
+
|---|---|
|
|
141
|
+
| `psdparse/psdbase.h` | Endian macros, type atoms (`u16str`), `IteratorBase`, `utf8ToWide` |
|
|
142
|
+
| `psdparse/psddata.h` | `Header`, `LayerInfo`, `ChannelInfo`, `ImageResourceInfo`, `LayerExtraData`, `Data` |
|
|
143
|
+
| `psdparse/psddesc.h` | Photoshop Descriptor data |
|
|
144
|
+
| `psdparse/psdparse.h` | `MemoryReader`, `StreamReader` (+ nested `Source`), `parsePSD` decl |
|
|
145
|
+
| `psdparse/psdparse.cpp` | The parser. `SubBlock` RAII, hand-rolled binary reading, `processParsed` post-parse fixup |
|
|
146
|
+
| `psdparse/psdfile.h` | `PSDFile` (load/save public API) |
|
|
147
|
+
| `psdparse/psdfile.cpp` | `PSDFile` impl, Win32 mmap pimpl, `IStreamSource` adapter |
|
|
148
|
+
| `psdparse/psdimage.cpp` | Per-channel and merged-image decoders (RLE / zip / raw) |
|
|
149
|
+
| `psdparse/psdwrite.h` | `WriterBase`, `FileWriter`, `writePSD` decl |
|
|
150
|
+
| `psdparse/psdwrite.cpp` | Writer implementation with patch-back size handling |
|
|
151
|
+
| `psdparse/psdresource.cpp` | Image resource handlers (slices, grids, color tables, layer comps) |
|
|
152
|
+
| `psdparse/psdlayer.cpp` | Layer-level additional info handlers (luni, lsct, lyid, …) |
|
|
153
|
+
| `psdparse/psddesc.cpp` | Descriptor parser |
|
|
154
|
+
| `psdparse/bmp.cpp` | BMP scratch-buffer helper |
|
|
155
|
+
| `psdparse/psd_cli.cpp` | Smoke-test CLI |
|
|
156
|
+
| `python/psdparse_module.cpp` | pybind11 bindings (PSDFile, LayerInfo, Header, enums) |
|