psdparse 0.2.2__tar.gz → 0.4.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.2.2 → psdparse-0.4.0}/PKG-INFO +6 -1
- {psdparse-0.2.2 → psdparse-0.4.0}/README.md +5 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/docs/PYTHON_API.md +128 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/docs/ROADMAP.md +6 -1
- psdparse-0.4.0/docs/SUPPORT.md +156 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/psdparse/psddata.h +5 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/psdparse/psdparse.cpp +8 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/pyproject.toml +1 -1
- psdparse-0.4.0/python/psdparse_module.cpp +557 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/tests/conftest.py +26 -0
- psdparse-0.4.0/tests/test_descriptors.py +103 -0
- psdparse-0.4.0/tests/test_metadata.py +104 -0
- psdparse-0.2.2/python/psdparse_module.cpp +0 -252
- {psdparse-0.2.2 → psdparse-0.4.0}/.gitignore +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/CMakeLists.txt +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/CMakePresets.json +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/LICENSE +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/Makefile +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/docs/ARCHITECTURE.md +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/psdparse/CMakeLists.txt +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/psdparse/bmp.cpp +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/psdparse/psd_cli.cpp +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/psdparse/psdbase.h +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/psdparse/psddesc.cpp +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/psdparse/psddesc.h +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/psdparse/psdengine.cpp +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/psdparse/psdengine.h +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/psdparse/psdfile.cpp +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/psdparse/psdfile.h +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/psdparse/psdimage.cpp +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/psdparse/psdlayer.cpp +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/psdparse/psdlayer.h +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/psdparse/psdparse.h +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/psdparse/psdresource.cpp +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/psdparse/psdresource.h +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/psdparse/psdwrite.cpp +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/psdparse/psdwrite.h +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/python/CMakeLists.txt +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/tests/test_header.py +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/tests/test_images.py +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/tests/test_layers.py +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/tests/test_save.py +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/tests/test_text.py +0 -0
- {psdparse-0.2.2 → psdparse-0.4.0}/tools/psd_export.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: psdparse
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Fast PSD (Photoshop) reader/writer — C++17 core with pybind11 bindings
|
|
5
5
|
Keywords: psd,photoshop,parser,image,graphics
|
|
6
6
|
Author-Email: wamsoft <wtnbgo@gmail.com>
|
|
@@ -133,6 +133,11 @@ p.save(r"out.psd") # byte-identical round-trip
|
|
|
133
133
|
|
|
134
134
|
Full API reference: [docs/PYTHON_API.md](docs/PYTHON_API.md).
|
|
135
135
|
|
|
136
|
+
## PSD feature coverage
|
|
137
|
+
|
|
138
|
+
What psdparse can and cannot read, at a glance:
|
|
139
|
+
[docs/SUPPORT.md](docs/SUPPORT.md) (対応状況マトリクス).
|
|
140
|
+
|
|
136
141
|
## Tests
|
|
137
142
|
|
|
138
143
|
Tests live under `tests/` and use [pytest](https://docs.pytest.org/). They need sample PSDs placed at the repo root or `tests/data/` (not committed — listed in `.gitignore`); without them the tests skip rather than fail.
|
|
@@ -92,6 +92,11 @@ p.save(r"out.psd") # byte-identical round-trip
|
|
|
92
92
|
|
|
93
93
|
Full API reference: [docs/PYTHON_API.md](docs/PYTHON_API.md).
|
|
94
94
|
|
|
95
|
+
## PSD feature coverage
|
|
96
|
+
|
|
97
|
+
What psdparse can and cannot read, at a glance:
|
|
98
|
+
[docs/SUPPORT.md](docs/SUPPORT.md) (対応状況マトリクス).
|
|
99
|
+
|
|
95
100
|
## Tests
|
|
96
101
|
|
|
97
102
|
Tests live under `tests/` and use [pytest](https://docs.pytest.org/). They need sample PSDs placed at the repo root or `tests/data/` (not committed — listed in `.gitignore`); without them the tests skip rather than fail.
|
|
@@ -101,7 +101,13 @@ Read-only view of one layer.
|
|
|
101
101
|
| `channels` | `list[ChannelInfo]` | per-channel id+length |
|
|
102
102
|
| `name` | `str` | raw Pascal-string name (CP932 etc on Japanese PSDs — pybind11 may raise UnicodeDecodeError when read) |
|
|
103
103
|
| `name_unicode` | `str` | UTF-16 Unicode name from `luni` record (preferred) |
|
|
104
|
+
| `parent_index` | `int` | index into `PSDFile.layers` of the enclosing folder, or `-1` for top level — see [Layer hierarchy](#layer-hierarchy) |
|
|
104
105
|
| `text` | `dict` \| `None` | text-layer content & style (`None` for non-text layers) — see below |
|
|
106
|
+
| `mask` | `dict` \| `None` | layer mask geometry & flags (`None` when the layer has no mask) — see below |
|
|
107
|
+
| `blending_ranges` | `dict` \| `None` | "Blend If" ranges (`None` when absent) — see below |
|
|
108
|
+
| `effects` | `dict` \| `None` | layer effects (`lfx2`) as a descriptor dict — see [Descriptor blocks](#descriptor-blocks) |
|
|
109
|
+
| `fill` | `dict` \| `None` | fill-layer content (solid/gradient/pattern) — see [Descriptor blocks](#descriptor-blocks) |
|
|
110
|
+
| `info_keys` | `list[str]` | 4cc keys of every additional-layer-info block on this layer |
|
|
105
111
|
| `visible` | `bool` | flag bit 1 inverted |
|
|
106
112
|
| `transparency_protected` | `bool` | flag bit 0 |
|
|
107
113
|
| `obsolete` | `bool` | flag bit 2 |
|
|
@@ -155,6 +161,128 @@ for layer in p.layers:
|
|
|
155
161
|
print(t["text"], "→", {r["font"] for r in t["runs"]})
|
|
156
162
|
```
|
|
157
163
|
|
|
164
|
+
### `layer.mask` — layer mask geometry & flags
|
|
165
|
+
|
|
166
|
+
`None` for layers without a mask. When present:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
{
|
|
170
|
+
"top": 8, "left": 10, "bottom": 40, "right": 50, # mask bbox on canvas
|
|
171
|
+
"width": 40, "height": 32,
|
|
172
|
+
"default_color": 0, # 0..255, area outside the stored mask rect
|
|
173
|
+
"flags": 0, # raw flag byte
|
|
174
|
+
"relative": False, # bit0: position relative to layer
|
|
175
|
+
"disabled": False, # bit1: mask disabled
|
|
176
|
+
"inverted": False, # bit2: invert (obsolete)
|
|
177
|
+
"from_render": False, # bit3: mask from rendering other data
|
|
178
|
+
"has_parameters": False, # bit4: density/feather present (not decoded yet)
|
|
179
|
+
"real": None, # or a nested dict (below) for a real/user mask
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
When the record carries a *real* (user + vector combined) mask, `real` is a dict
|
|
184
|
+
with `flags`, `background`, and the enclosing `top/left/bottom/right`. The mask
|
|
185
|
+
**pixels** are unchanged from before — fetch them with
|
|
186
|
+
`p.layer_image(i, "mask")`.
|
|
187
|
+
|
|
188
|
+
### `layer.blending_ranges` — "Blend If" ranges
|
|
189
|
+
|
|
190
|
+
`None` when absent. `gray` is the composite range; `channels` has one entry per
|
|
191
|
+
channel. Each value is the raw 32-bit packed range (two 16-bit black/white
|
|
192
|
+
sub-ranges — split yourself if you need the individual sliders):
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
{
|
|
196
|
+
"gray": (65535, 65535), # (source, dest)
|
|
197
|
+
"channels": [(0, 65535), (0, 65535), ...],
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Layer hierarchy
|
|
202
|
+
|
|
203
|
+
Layers are a **flat list in file order**; folder structure is recovered from
|
|
204
|
+
`layer.parent_index` (`-1` = top level, otherwise the index of the enclosing
|
|
205
|
+
`FOLDER` layer). To build a tree:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
children = {i: [] for i in range(len(p.layers))}
|
|
209
|
+
roots = []
|
|
210
|
+
for i, l in enumerate(p.layers):
|
|
211
|
+
(roots if l.parent_index == -1 else children[l.parent_index]).append(i)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`FOLDER` marks a group's start and the matching `HIDDEN` layer marks its end
|
|
215
|
+
(these are Photoshop's `lsct` section dividers).
|
|
216
|
+
|
|
217
|
+
## Document resources
|
|
218
|
+
|
|
219
|
+
Read-only accessors on `PSDFile` for whole-document metadata. Each returns
|
|
220
|
+
`None` (or an empty list) when the PSD lacks that resource.
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
p.guides # dict|None : {"horizontal_grid", "vertical_grid", "guides":[{"location","direction"}]}
|
|
224
|
+
p.slices # dict|None : {"group_name", "bounding":{...}, "slices":[{...}]}
|
|
225
|
+
p.layer_comps # list[dict]: [{"id","name","comment","record_visibility","record_position","record_appearance"}]
|
|
226
|
+
p.color_table # dict|None : {"colors":[(r,g,b,a)], "valid_count", "transparency_index"} for indexed-color PSDs
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
- **`guides`** — grid spacing (in 1/32 px) and each guide's `location` (1/32 px
|
|
230
|
+
from origin) and `direction` (`"vertical"` / `"horizontal"`).
|
|
231
|
+
- **`slices`** — Photoshop slices (v6). Each slice has `id`, `name`, bbox
|
|
232
|
+
(`left/top/right/bottom`), `url`, `target`, `message`, `alt_tag`, `cell_text`,
|
|
233
|
+
alignment, and an `(r, g, b, a)` `color` tuple.
|
|
234
|
+
- **`color_table`** — only present for `COLOR_MODE_INDEXED` PSDs; `colors` is the
|
|
235
|
+
palette and `transparency_index` is `-1` when there is no transparent entry.
|
|
236
|
+
|
|
237
|
+
## Descriptor blocks
|
|
238
|
+
|
|
239
|
+
Photoshop stores layer **effects**, **fill-layer** content and many other
|
|
240
|
+
tagged blocks as its generic *descriptor* tree (the same OSType structure used
|
|
241
|
+
throughout PSD). These accessors decode a block into nested Python
|
|
242
|
+
dicts/lists so they can be read without a decoder per feature.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
layer.effects # dict|None : object-based effects ('lfx2')
|
|
246
|
+
layer.fill # dict|None : {"type": "solid"|"gradient"|"pattern", "data": {...}}
|
|
247
|
+
layer.info_keys # list[str] : every additional-info 4cc key present on the layer
|
|
248
|
+
layer.descriptor(key, skip=-1) # dict|None : parse an arbitrary key as a descriptor
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Value mapping** (descriptor item → Python):
|
|
252
|
+
|
|
253
|
+
| Descriptor type | Python |
|
|
254
|
+
|---|---|
|
|
255
|
+
| Integer / Double | `int` / `float` |
|
|
256
|
+
| Boolean | `bool` |
|
|
257
|
+
| String / Alias / Class | `str` |
|
|
258
|
+
| UnitFloat | `{"value": float, "unit": str}` (unit: `percent`, `angle`, `pixels`, …) |
|
|
259
|
+
| Enumerated | `{"type": str, "value": str}` |
|
|
260
|
+
| Descriptor (nested) | `dict` (keys are raw 4cc, **may end in a space**, e.g. `"Scl "`) |
|
|
261
|
+
| List | `list` |
|
|
262
|
+
| RawData (`tdta`) | `bytes` |
|
|
263
|
+
| Reference / unknown | `None` |
|
|
264
|
+
|
|
265
|
+
```python
|
|
266
|
+
fx = layer.effects
|
|
267
|
+
if fx:
|
|
268
|
+
print("effects on:", fx.get("masterFXSwitch"))
|
|
269
|
+
po = fx.get("patternFill") # a nested descriptor dict
|
|
270
|
+
if po:
|
|
271
|
+
print("pattern overlay opacity:", po["Opct"]["value"]) # -> 100.0
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Notes:
|
|
275
|
+
- Keys are the **raw 4cc** as stored (trailing spaces preserved) — index with
|
|
276
|
+
the exact string, e.g. `fx["Scl "]`, `color["Rd "]`.
|
|
277
|
+
- `layer.effects` is `lfx2` (object-based, Photoshop 6+). The older binary
|
|
278
|
+
`lrFX` block is **not** a descriptor and returns `None` via `descriptor()`.
|
|
279
|
+
- `descriptor(key, skip)` is the generic escape hatch: `skip` is the number of
|
|
280
|
+
version-prefix bytes before the descriptor (`-1` auto-detects for `lfx2` = 8
|
|
281
|
+
and `SoCo`/`GdFl`/`PtFl` = 4, otherwise 0). Use `info_keys` to discover which
|
|
282
|
+
blocks a layer carries.
|
|
283
|
+
- Decoding is **lazy** — the descriptor is parsed from the block's raw bytes on
|
|
284
|
+
each access, so cache the result if you read it repeatedly.
|
|
285
|
+
|
|
158
286
|
## Enums
|
|
159
287
|
|
|
160
288
|
```python
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# psdparse Roadmap
|
|
2
2
|
|
|
3
|
+
For a feature-by-feature account of what is and isn't supported today, see
|
|
4
|
+
[SUPPORT.md](SUPPORT.md). This file tracks planned work.
|
|
5
|
+
|
|
3
6
|
## Current state (2026-06-12)
|
|
4
7
|
|
|
5
8
|
- ✅ Pure C++17 parser (no Boost)
|
|
@@ -78,8 +81,10 @@ This is mostly a constructor that fills `Data` with a minimal-but-valid skeleton
|
|
|
78
81
|
- **Warp text** — lives in the `TySh` *warp* descriptor (currently skipped, not in EngineData); needs samples with each warp style + non-zero bend/distortion.
|
|
79
82
|
- **Area (paragraph) vs point text / text box bounds** and **text-on-path** — need samples.
|
|
80
83
|
- **Leading / faux bold-italic / underline / strikethrough / paragraph indent+spacing** — keys exist in EngineData but are default-valued in the current sample, so per-run extraction can't be verified yet; needs a sample authored with non-default values.
|
|
84
|
+
- ✅ **Tier-1 reference metadata exposed to Python.** *Done 2026-08-02 (0.3.0).* Data the C++ core already parsed but Python couldn't reach is now bound: `layer.parent_index` (folder hierarchy), `layer.mask` (bbox / flags / real-mask dict), `layer.blending_ranges`, and document-level `PSDFile.guides` / `.slices` / `.layer_comps` / `.color_table`. All dict-shaped, matching the existing `layer.text` style. Validated against `config.psd` / `system.psd` (hierarchy, blend ranges, guides, slices) and a psd-tools-synthesized `masktest.psd` (mask bbox/flags cross-checked) — see `tests/test_metadata.py`.
|
|
85
|
+
- ✅ **Tier-2 generic Descriptor → dict bridge.** *Done 2026-08-02 (0.4.0).* `layer.effects` (`lfx2`), `layer.fill` (`SoCo`/`GdFl`/`PtFl`), `layer.info_keys`, and a generic `layer.descriptor(key, skip)` escape hatch route previously-skipped tagged blocks through the existing complete descriptor parser (`psddesc.*`). A `Descriptor → py::dict` converter in the binding (dynamic_cast dispatch; UnitFloat→`{value,unit}`, Enumerated→`{type,value}`, nested Descriptor→dict, List→list, tdta→bytes) means no per-feature decoders were needed. Decoding is lazy: each access clones the block's reader (`clone()`+`init()`) and re-parses, so `save()` round-trips remain byte-identical. Cross-checked value-for-value against psd-tools 1.17 on `config.psd`'s PatternOverlay (opacity/scale/angle/blend/pattern-name all matched) — see `tests/test_descriptors.py`. **Deferred:** typed high-level effect/fill objects, smart-object `SoLd`/`lnkD` (embedded-file extraction), and binary adjustment layers (`levl`/`curv`) all still need work — the raw descriptor dict is the current interface.
|
|
81
86
|
- 16-bit (`Lr16`) and 32-bit-float (`Lr32`) layer data: currently captured in `layerAndMaskTrailing` for round-trip but not exposed as decoded pixels.
|
|
82
|
-
- Layer mask:
|
|
87
|
+
- Layer mask: density / feather decoding (the `has_parameters` flag is exposed but the `MaskParameters` values are not read yet), plus re-emission for masks > 20 bytes.
|
|
83
88
|
- Image resources: most are currently passed through as raw bytes. Higher-level accessors for ICC profile, EXIF, thumbnail, version info, etc. would be nice for tools.
|
|
84
89
|
- CMYK / Lab / Indexed color extraction in `getLayerImage` — currently optimized for RGB.
|
|
85
90
|
- Linux / macOS testing. mmap path uses POSIX `mmap` but hasn't been built / tested there.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# psdparse PSD 対応状況 (Support Matrix)
|
|
2
|
+
|
|
3
|
+
psdparse が PSD のどの機能を、どの水準で扱えるかの一覧です。psdparse は
|
|
4
|
+
**参照 (読み取り) + ラウンドトリップ保存** に重点を置いた実装で、編集保存や
|
|
5
|
+
高度な再合成は対象外です。
|
|
6
|
+
|
|
7
|
+
対応状況の凡例:
|
|
8
|
+
|
|
9
|
+
| 記号 | 意味 |
|
|
10
|
+
|:---:|---|
|
|
11
|
+
| ✅ | 対応済み — 構造化して取得できる |
|
|
12
|
+
| 🟡 | 部分対応 — 一部の値のみ / 生 descriptor 経由 |
|
|
13
|
+
| 📦 | 生バイトのみ保持 (ラウンドトリップ用。Python へは未公開) |
|
|
14
|
+
| ❌ | 未対応 — パースしていない |
|
|
15
|
+
|
|
16
|
+
最終更新: 2026-08-02 (v0.4.0)。詳細な今後の計画は [ROADMAP.md](ROADMAP.md)、
|
|
17
|
+
Python API の使い方は [PYTHON_API.md](PYTHON_API.md) を参照。
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## ファイル全体
|
|
22
|
+
|
|
23
|
+
| 機能 | 状況 | 備考 |
|
|
24
|
+
|---|:---:|---|
|
|
25
|
+
| ヘッダ (幅/高さ/チャンネル/深度/モード/版) | ✅ | `PSDFile.header` |
|
|
26
|
+
| 解像度 (dpi, image resource 1005) | ✅ | `header.hres` / `header.vres` |
|
|
27
|
+
| PSB (large document, version 2) | ❌ | `version` は読めるが、PSB 特有の 8 byte 長フィールドの分岐が無く未対応 |
|
|
28
|
+
| ラウンドトリップ保存 (byte-identical) | ✅ | `load(a) -> save(b)` が完全一致 |
|
|
29
|
+
| 編集して保存 (レイヤ追加/削除/差し替え) | ❌ | ROADMAP Phase 4b–4e。RLE エンコーダ未実装 |
|
|
30
|
+
|
|
31
|
+
## 圧縮 / ビット深度
|
|
32
|
+
|
|
33
|
+
| 項目 | 状況 | 備考 |
|
|
34
|
+
|---|:---:|---|
|
|
35
|
+
| Raw / RLE(PackBits) / ZIP(±prediction) | ✅ | 展開対応 |
|
|
36
|
+
| ビット深度 1 / 8 / 16 / 32 | ✅ | それ以外は非対応 |
|
|
37
|
+
|
|
38
|
+
## カラーモード (ピクセル展開: `merged_image` / `layer_image`)
|
|
39
|
+
|
|
40
|
+
| モード | 状況 | 備考 |
|
|
41
|
+
|---|:---:|---|
|
|
42
|
+
| Bitmap (1bit) | ✅ | |
|
|
43
|
+
| Grayscale (8/16/32) | ✅ | |
|
|
44
|
+
| RGB (8/16/32) | ✅ | |
|
|
45
|
+
| Indexed (8bit) | ✅ | パレットは `PSDFile.color_table` |
|
|
46
|
+
| CMYK (8/16/32) | ✅ | RGB へ変換して出力 |
|
|
47
|
+
| Multichannel | ❌ | `getLayerImage` 未対応 |
|
|
48
|
+
| Duotone | ❌ | 同上 |
|
|
49
|
+
| Lab | ❌ | 同上 |
|
|
50
|
+
|
|
51
|
+
出力は常に **BGRA インターリーブ** (4 byte/px)。ICC を用いた色変換は行いません。
|
|
52
|
+
|
|
53
|
+
## 画像取得
|
|
54
|
+
|
|
55
|
+
| 機能 | 状況 | 備考 |
|
|
56
|
+
|---|:---:|---|
|
|
57
|
+
| 合成画像 (merged/composite) | ✅ | `merged_image()` — PSD 保存済みの合成を返す |
|
|
58
|
+
| レイヤ画像 (mask 込み/無し/mask のみ) | ✅ | `layer_image(i, "masked"/"image"/"mask")` |
|
|
59
|
+
| 効果・調整を適用した再合成 (`composite()` 相当) | ❌ | 保存済み合成の取得のみ。再描画はしない |
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## レイヤ属性
|
|
64
|
+
|
|
65
|
+
| 属性 | 状況 | API |
|
|
66
|
+
|---|:---:|---|
|
|
67
|
+
| 位置/サイズ (bbox, width, height) | ✅ | `top/left/bottom/right`, `width`, `height` |
|
|
68
|
+
| ブレンドモード | ✅ | `blend_mode` (enum), `blend_mode_key` |
|
|
69
|
+
| 不透明度 / 塗り不透明度 | ✅ | `opacity`, `fill_opacity` |
|
|
70
|
+
| クリッピング | ✅ | `clipping` |
|
|
71
|
+
| 可視/ロック等フラグ | ✅ | `visible`, `transparency_protected`, `obsolete`, `pixel_data_irrelevant` |
|
|
72
|
+
| レイヤ名 (raw / Unicode) | ✅ | `name`, `name_unicode` |
|
|
73
|
+
| レイヤ ID | ✅ | `layer_id` |
|
|
74
|
+
| レイヤ種別 | ✅ | `layer_type` (NORMAL/HIDDEN/FOLDER/ADJUST/FILL/TEXT) |
|
|
75
|
+
| **フォルダ階層 (親子)** | ✅ | `parent_index` (v0.3.0) |
|
|
76
|
+
| チャンネル構成 (id/length) | ✅ | `channels[]` |
|
|
77
|
+
| シートカラー (レイヤパネルの色ラベル, `lclr`) | ❌ | 未デコード |
|
|
78
|
+
|
|
79
|
+
## レイヤマスク / ブレンド範囲
|
|
80
|
+
|
|
81
|
+
| 機能 | 状況 | API |
|
|
82
|
+
|---|:---:|---|
|
|
83
|
+
| マスク矩形/フラグ/既定色 | ✅ | `layer.mask` (v0.3.0) |
|
|
84
|
+
| real/user mask (>20byte) | ✅ | `layer.mask["real"]` |
|
|
85
|
+
| density / feather | 🟡 | `has_parameters` フラグのみ公開。値 (MaskParameters) は未デコード |
|
|
86
|
+
| ベクタマスク / パス (`vmsk`/`vsms`) | ❌ | 未対応 |
|
|
87
|
+
| ブレンディングレンジ ("Blend If") | ✅ | `layer.blending_ranges` (raw 32bit packed) |
|
|
88
|
+
|
|
89
|
+
## テキストレイヤ
|
|
90
|
+
|
|
91
|
+
| 機能 | 状況 | 備考 |
|
|
92
|
+
|---|:---:|---|
|
|
93
|
+
| 本文 / 縦横 / 変換行列 | ✅ | `layer.text` (`text`/`orientation`/`transform`) |
|
|
94
|
+
| ラン単位スタイル (font/size/color/tracking/kerning) | ✅ | `text["runs"]` |
|
|
95
|
+
| 段落別の行揃え | ✅ | `text["paragraphs"]` (v0.2.2) |
|
|
96
|
+
| ワープ (warp) | ❌ | TySh warp descriptor 未処理 |
|
|
97
|
+
| 非 RGB の FillColor | ❌ | `/Type 1` (RGB) のみ |
|
|
98
|
+
| leading / 疑似ボールド / 下線 等 | 🟡 | EngineData にキーはあるが既定値サンプルのみで未検証 |
|
|
99
|
+
|
|
100
|
+
## Descriptor ブロック (v0.4.0)
|
|
101
|
+
|
|
102
|
+
Photoshop の汎用ディスクリプタで格納されるブロックを dict 化して取得。
|
|
103
|
+
|
|
104
|
+
| 機能 | 状況 | API |
|
|
105
|
+
|---|:---:|---|
|
|
106
|
+
| レイヤー効果 (`lfx2`, object-based) | ✅ | `layer.effects` (nested descriptor dict) |
|
|
107
|
+
| 旧レイヤー効果 (`lrFX`, binary) | ❌ | descriptor でないため未対応 |
|
|
108
|
+
| 塗りつぶしレイヤ (`SoCo`/`GdFl`/`PtFl`) | ✅ | `layer.fill` (`{type, data}`) |
|
|
109
|
+
| 任意キーの descriptor 取得 | ✅ | `layer.descriptor(key, skip)` / `layer.info_keys` |
|
|
110
|
+
|
|
111
|
+
`effects` / `fill` は **生 descriptor を辞書化したもの** で、効果ごとの型付き
|
|
112
|
+
アクセサ (drop shadow の angle/distance を名前で、等) はまだありません。値の
|
|
113
|
+
マッピング規則は [PYTHON_API.md](PYTHON_API.md#descriptor-blocks) を参照。
|
|
114
|
+
|
|
115
|
+
## 調整レイヤ / 塗りつぶし / スマートオブジェクト / シェイプ
|
|
116
|
+
|
|
117
|
+
| 機能 | 状況 | 備考 |
|
|
118
|
+
|---|:---:|---|
|
|
119
|
+
| 調整レイヤの種別判定 | ✅ | `layer_type == ADJUST` |
|
|
120
|
+
| 調整レイヤのパラメータ (levels/curves 等, binary) | ❌ | 未デコード。descriptor 形式のもの (`CgEd` 等) は `descriptor()` で取得可 |
|
|
121
|
+
| スマートオブジェクト変換 (`SoLd`/`PlLd`) | 🟡 | `descriptor()` で試行可 (未検証、既定 skip 要調整) |
|
|
122
|
+
| スマートオブジェクト埋め込みデータ抽出 (`lnkD`) | ❌ | 未対応 |
|
|
123
|
+
| ベクタストローク/シェイプ (`vstk`/`vscg`) | 🟡 | `descriptor()` 経由で取得可 (未検証) |
|
|
124
|
+
| ライブシェイプ情報 (origination) | ❌ | 未対応 |
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Image Resources / 文書メタデータ
|
|
129
|
+
|
|
130
|
+
| リソース | 状況 | API |
|
|
131
|
+
|---|:---:|---|
|
|
132
|
+
| 解像度 (1005) | ✅ | `header.hres/vres` |
|
|
133
|
+
| グリッド & ガイド (1032) | ✅ | `PSDFile.guides` (v0.3.0) |
|
|
134
|
+
| スライス (1050 v6) | ✅ | `PSDFile.slices` (v0.3.0) |
|
|
135
|
+
| スライス (1050 v7/v8, descriptor) | ❌ | 未格納 |
|
|
136
|
+
| レイヤーカンプ (1065) | ✅ | `PSDFile.layer_comps` (v0.3.0) |
|
|
137
|
+
| インデックスカラーパレット (色/count/透明index) | ✅ | `PSDFile.color_table` (v0.3.0) |
|
|
138
|
+
| ICC プロファイル (1039) | 📦 | 生バイト保持のみ・未公開 |
|
|
139
|
+
| EXIF (1058/1059) | 📦 | 同上 |
|
|
140
|
+
| XMP メタデータ (1060) | 📦 | 同上 |
|
|
141
|
+
| サムネイル (1033/1036) | 📦 | 同上 |
|
|
142
|
+
| バージョン情報 / アルファチャンネル名 / その他 | 📦 | 同上 |
|
|
143
|
+
| Global layer mask info | 📦 | 構造体フィールドは未充填、生バイトのみ |
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## まとめ (一言で)
|
|
148
|
+
|
|
149
|
+
- **得意**: レイヤ列挙 + 属性 + 階層、RGB/CMYK/Gray/Indexed のピクセル取得、
|
|
150
|
+
テキストのラン単位スタイル、レイヤー効果/塗りの descriptor、主要な文書
|
|
151
|
+
メタデータ (ガイド/スライス/カンプ)、そして **byte-identical なラウンドトリップ**。
|
|
152
|
+
- **未対応/限定的**: Lab/Duotone/Multichannel ピクセル、調整レイヤの数値
|
|
153
|
+
パラメータ、ベクタパス、スマートオブジェクト実体、効果込みの再合成、
|
|
154
|
+
そして編集して保存。
|
|
155
|
+
- 生バイトは全ブロックが保持されているので、📦 の項目は「公開するだけ」で
|
|
156
|
+
対応できるものが多い (ROADMAP 参照)。
|
|
@@ -287,6 +287,8 @@ namespace psd {
|
|
|
287
287
|
};
|
|
288
288
|
|
|
289
289
|
struct LayerMask {
|
|
290
|
+
bool present = false; // マスクブロック (size>0) が存在したか
|
|
291
|
+
bool hasReal = false; // real/user mask (size>20) を含むか
|
|
290
292
|
int width;
|
|
291
293
|
int height;
|
|
292
294
|
int top;
|
|
@@ -309,6 +311,7 @@ namespace psd {
|
|
|
309
311
|
};
|
|
310
312
|
|
|
311
313
|
struct LayerBlendingRange {
|
|
314
|
+
bool present = false; // blending range ブロック (size>0) が存在したか
|
|
312
315
|
int grayBlendSource;
|
|
313
316
|
int grayBlendDest;
|
|
314
317
|
std::vector<LayerBlendingChannel> channels;
|
|
@@ -477,6 +480,8 @@ namespace psd {
|
|
|
477
480
|
|
|
478
481
|
// 親フォルダレイヤ
|
|
479
482
|
LayerInfo *parent;
|
|
483
|
+
// 親フォルダの layerList インデックス (-1 = トップレベル)。processParsed で設定。
|
|
484
|
+
int parentIndex = -1;
|
|
480
485
|
|
|
481
486
|
bool isTransparencyProtected() const { return (flag & (1 << 0)) != 0; }
|
|
482
487
|
bool isVisible() const { return (flag & (1 << 1)) == 0; }
|
|
@@ -290,16 +290,21 @@ Data::processParsed()
|
|
|
290
290
|
|
|
291
291
|
// グループ情報をリンク
|
|
292
292
|
std::stack<LayerInfo*> parent;
|
|
293
|
+
std::stack<int> parentIdx;
|
|
293
294
|
parent.push(0);
|
|
295
|
+
parentIdx.push(-1);
|
|
294
296
|
for (int i = (int)layerList.size() - 1; i >= 0; i--) {
|
|
295
297
|
LayerInfo *layer = &layerList[i];
|
|
296
298
|
layer->parent = parent.top();
|
|
299
|
+
layer->parentIndex = parentIdx.top();
|
|
297
300
|
switch (layer->layerType) {
|
|
298
301
|
case LAYER_TYPE_FOLDER:
|
|
299
302
|
parent.push(layer);
|
|
303
|
+
parentIdx.push(i);
|
|
300
304
|
break;
|
|
301
305
|
case LAYER_TYPE_HIDDEN:
|
|
302
306
|
parent.pop();
|
|
307
|
+
if (parentIdx.size() > 1) parentIdx.pop();
|
|
303
308
|
break;
|
|
304
309
|
default:
|
|
305
310
|
break;
|
|
@@ -449,6 +454,7 @@ void parseLayerMask(IteratorBase &r, LayerMask &m, int size) {
|
|
|
449
454
|
// is realFlags; the original grammar consumed a byte before realFlags too.
|
|
450
455
|
(void)r.getCh();
|
|
451
456
|
if (size > 20) {
|
|
457
|
+
m.hasReal = true;
|
|
452
458
|
m.realFlags = r.getCh();
|
|
453
459
|
m.realUserMaskBackground = r.getCh();
|
|
454
460
|
m.enclosingTop = r.getInt32(true);
|
|
@@ -458,9 +464,11 @@ void parseLayerMask(IteratorBase &r, LayerMask &m, int size) {
|
|
|
458
464
|
}
|
|
459
465
|
m.width = m.right - m.left;
|
|
460
466
|
m.height = m.bottom - m.top;
|
|
467
|
+
m.present = true;
|
|
461
468
|
}
|
|
462
469
|
|
|
463
470
|
void parseLayerBlendingRange(IteratorBase &r, LayerBlendingRange &b) {
|
|
471
|
+
b.present = true;
|
|
464
472
|
b.grayBlendSource = r.getInt32(true);
|
|
465
473
|
b.grayBlendDest = r.getInt32(true);
|
|
466
474
|
while (r.rest() >= 8) {
|