psdparse 0.4.0__tar.gz → 0.5.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.4.0 → psdparse-0.5.0}/PKG-INFO +1 -1
- {psdparse-0.4.0 → psdparse-0.5.0}/docs/PYTHON_API.md +28 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/docs/ROADMAP.md +1 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/docs/SUPPORT.md +13 -9
- {psdparse-0.4.0 → psdparse-0.5.0}/pyproject.toml +1 -1
- {psdparse-0.4.0 → psdparse-0.5.0}/python/psdparse_module.cpp +92 -1
- psdparse-0.5.0/tests/test_resources.py +69 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/.gitignore +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/CMakeLists.txt +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/CMakePresets.json +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/LICENSE +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/Makefile +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/README.md +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/docs/ARCHITECTURE.md +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/psdparse/CMakeLists.txt +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/psdparse/bmp.cpp +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/psdparse/psd_cli.cpp +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/psdparse/psdbase.h +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/psdparse/psddata.h +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/psdparse/psddesc.cpp +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/psdparse/psddesc.h +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/psdparse/psdengine.cpp +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/psdparse/psdengine.h +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/psdparse/psdfile.cpp +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/psdparse/psdfile.h +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/psdparse/psdimage.cpp +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/psdparse/psdlayer.cpp +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/psdparse/psdlayer.h +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/psdparse/psdparse.cpp +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/psdparse/psdparse.h +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/psdparse/psdresource.cpp +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/psdparse/psdresource.h +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/psdparse/psdwrite.cpp +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/psdparse/psdwrite.h +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/python/CMakeLists.txt +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/tests/conftest.py +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/tests/test_descriptors.py +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/tests/test_header.py +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/tests/test_images.py +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/tests/test_layers.py +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/tests/test_metadata.py +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/tests/test_save.py +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/tests/test_text.py +0 -0
- {psdparse-0.4.0 → psdparse-0.5.0}/tools/psd_export.py +0 -0
|
@@ -226,6 +226,34 @@ p.layer_comps # list[dict]: [{"id","name","comment","record_visibility","recor
|
|
|
226
226
|
p.color_table # dict|None : {"colors":[(r,g,b,a)], "valid_count", "transparency_index"} for indexed-color PSDs
|
|
227
227
|
```
|
|
228
228
|
|
|
229
|
+
### Image resources (raw)
|
|
230
|
+
|
|
231
|
+
Most image resources are exposed as their raw on-disk bytes; decoding (EXIF
|
|
232
|
+
tags, rendering the thumbnail, parsing the ICC profile) is left to the caller.
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
p.image_resource_ids # list[int] : IDs of every resource present
|
|
236
|
+
p.image_resource(id) # bytes|None : raw bytes of the resource with that ID
|
|
237
|
+
p.icc_profile # bytes|None : ICC profile (resource 1039)
|
|
238
|
+
p.exif # bytes|None : EXIF block (1058)
|
|
239
|
+
p.xmp # str|None : XMP packet (1060), UTF-8 XML
|
|
240
|
+
p.thumbnail # dict|None : {"format","width","height","bits","resource_id","data"}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
- **`thumbnail`** — resource 1036 (RGB) or legacy 1033 (BGR). When
|
|
244
|
+
`format == "jpeg"`, `data` is JFIF JPEG bytes ready for `PIL.Image.open`:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
import io
|
|
248
|
+
from PIL import Image
|
|
249
|
+
th = p.thumbnail
|
|
250
|
+
if th and th["format"] == "jpeg":
|
|
251
|
+
Image.open(io.BytesIO(th["data"])).save("thumb.png")
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
- **`xmp`** decodes as UTF-8 `str`; if a file's packet is not valid UTF-8, read
|
|
255
|
+
the raw bytes with `p.image_resource(1060)` instead.
|
|
256
|
+
|
|
229
257
|
- **`guides`** — grid spacing (in 1/32 px) and each guide's `location` (1/32 px
|
|
230
258
|
from origin) and `direction` (`"vertical"` / `"horizontal"`).
|
|
231
259
|
- **`slices`** — Photoshop slices (v6). Each slice has `id`, `name`, bbox
|
|
@@ -83,6 +83,7 @@ This is mostly a constructor that fills `Data` with a minimal-but-valid skeleton
|
|
|
83
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
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
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.
|
|
86
|
+
- ✅ **Image-resource raw bytes exposed.** *Done 2026-08-02 (0.5.0).* `PSDFile.image_resource(id)` / `.image_resource_ids` plus typed shortcuts `.icc_profile` (1039), `.exif` (1058), `.xmp` (1060, UTF-8 str) and `.thumbnail` (1036/1033 → dict with JPEG bytes + dimensions). Raw bytes only — decoding EXIF tags / rendering the thumbnail is left to the caller. EXIF and ICC bytes cross-checked byte-identical against psd-tools 1.17; thumbnail JPEG verified decodable with Pillow — see `tests/test_resources.py`. **Still raw-only / unexposed:** higher-level decode of these (parsed EXIF, ICC transform) and the structured `GlobalLayerMaskInfo`.
|
|
86
87
|
- 16-bit (`Lr16`) and 32-bit-float (`Lr32`) layer data: currently captured in `layerAndMaskTrailing` for round-trip but not exposed as decoded pixels.
|
|
87
88
|
- 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.
|
|
88
89
|
- 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.
|
|
@@ -135,11 +135,13 @@ Photoshop の汎用ディスクリプタで格納されるブロックを dict
|
|
|
135
135
|
| スライス (1050 v7/v8, descriptor) | ❌ | 未格納 |
|
|
136
136
|
| レイヤーカンプ (1065) | ✅ | `PSDFile.layer_comps` (v0.3.0) |
|
|
137
137
|
| インデックスカラーパレット (色/count/透明index) | ✅ | `PSDFile.color_table` (v0.3.0) |
|
|
138
|
-
| ICC プロファイル (1039) |
|
|
139
|
-
| EXIF (1058
|
|
140
|
-
| XMP メタデータ (1060) |
|
|
141
|
-
| サムネイル (1033
|
|
142
|
-
|
|
|
138
|
+
| ICC プロファイル (1039) | ✅ | `PSDFile.icc_profile` (生バイト, v0.5.0) |
|
|
139
|
+
| EXIF (1058) | ✅ | `PSDFile.exif` (生バイト, v0.5.0) |
|
|
140
|
+
| XMP メタデータ (1060) | ✅ | `PSDFile.xmp` (UTF-8 str, v0.5.0) |
|
|
141
|
+
| サムネイル (1036/1033) | ✅ | `PSDFile.thumbnail` (JPEG bytes + 寸法, v0.5.0) |
|
|
142
|
+
| 任意リソースの生バイト | ✅ | `PSDFile.image_resource(id)` / `image_resource_ids` (v0.5.0) |
|
|
143
|
+
| 上記の内容デコード (EXIF タグ解析等) | ❌ | 生バイトを返すのみ。解析は利用側 (Pillow 等) で |
|
|
144
|
+
| バージョン情報 / アルファチャンネル名 / その他 | 📦→✅ | `image_resource(id)` で生バイト取得可 |
|
|
143
145
|
| Global layer mask info | 📦 | 構造体フィールドは未充填、生バイトのみ |
|
|
144
146
|
|
|
145
147
|
---
|
|
@@ -147,10 +149,12 @@ Photoshop の汎用ディスクリプタで格納されるブロックを dict
|
|
|
147
149
|
## まとめ (一言で)
|
|
148
150
|
|
|
149
151
|
- **得意**: レイヤ列挙 + 属性 + 階層、RGB/CMYK/Gray/Indexed のピクセル取得、
|
|
150
|
-
テキストのラン単位スタイル、レイヤー効果/塗りの descriptor
|
|
151
|
-
|
|
152
|
+
テキストのラン単位スタイル、レイヤー効果/塗りの descriptor、文書メタデータ
|
|
153
|
+
(ガイド/スライス/カンプ) と image resource の生バイト (ICC/EXIF/XMP/サムネイル)、
|
|
154
|
+
そして **byte-identical なラウンドトリップ**。
|
|
152
155
|
- **未対応/限定的**: Lab/Duotone/Multichannel ピクセル、調整レイヤの数値
|
|
153
156
|
パラメータ、ベクタパス、スマートオブジェクト実体、効果込みの再合成、
|
|
154
157
|
そして編集して保存。
|
|
155
|
-
-
|
|
156
|
-
|
|
158
|
+
- image resource の**中身の解釈** (EXIF タグ, サムネイル描画等) は行わず生バイトを
|
|
159
|
+
返すのみ。残る 📦 (global layer mask info の構造化) は「公開するだけ」で対応可
|
|
160
|
+
(ROADMAP 参照)。
|
|
@@ -346,6 +346,82 @@ py::object layerDescriptor(const psd::LayerInfo &l, const std::string &keyStr, i
|
|
|
346
346
|
return keyDescriptor(l, key, skip);
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
// -------------------------------------------------------------------------
|
|
350
|
+
// Image resource raw-bytes access.
|
|
351
|
+
//
|
|
352
|
+
// Most image resources are kept as raw bytes internally (imageResourceList)
|
|
353
|
+
// but were never reachable from Python. These helpers expose them: a generic
|
|
354
|
+
// by-ID accessor plus typed shortcuts for the common ones (ICC / EXIF / XMP /
|
|
355
|
+
// thumbnail). Decoding (parsing EXIF tags, rendering the thumbnail) is left to
|
|
356
|
+
// the caller with e.g. Pillow.
|
|
357
|
+
// -------------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
std::string resourceBytes(const psd::ImageResourceInfo &res) {
|
|
360
|
+
std::string buf((size_t)(res.size > 0 ? res.size : 0), '\0');
|
|
361
|
+
if (res.size > 0 && res.data) {
|
|
362
|
+
psd::IteratorBase *rd = res.data->clone();
|
|
363
|
+
rd->init(); // rewind to the start of this resource
|
|
364
|
+
rd->getData(&buf[0], res.size);
|
|
365
|
+
delete rd;
|
|
366
|
+
}
|
|
367
|
+
return buf;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const psd::ImageResourceInfo *findResource(const psd::PSDFile &self, int id) {
|
|
371
|
+
for (const auto &res : self.imageResourceList)
|
|
372
|
+
if (res.id == id) return &res;
|
|
373
|
+
return nullptr;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
py::list imageResourceIds(psd::PSDFile &self) {
|
|
377
|
+
py::list out;
|
|
378
|
+
for (const auto &res : self.imageResourceList) out.append((int)res.id);
|
|
379
|
+
return out;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
py::object imageResource(psd::PSDFile &self, int id) {
|
|
383
|
+
const psd::ImageResourceInfo *res = findResource(self, id);
|
|
384
|
+
return res ? py::object(py::bytes(resourceBytes(*res))) : py::none();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
py::object iccProfile(psd::PSDFile &self) { return imageResource(self, 1039); }
|
|
388
|
+
py::object exifData(psd::PSDFile &self) { return imageResource(self, 1058); }
|
|
389
|
+
|
|
390
|
+
// XMP packet (resource 1060) is UTF-8 XML. Returned as str; use
|
|
391
|
+
// image_resource(1060) for the raw bytes if the packet is not valid UTF-8.
|
|
392
|
+
py::object xmpMetadata(psd::PSDFile &self) {
|
|
393
|
+
const psd::ImageResourceInfo *res = findResource(self, 1060);
|
|
394
|
+
if (!res) return py::none();
|
|
395
|
+
return py::str(resourceBytes(*res));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Embedded thumbnail (resource 1036 = RGB / legacy 1033 = BGR). Header is 28
|
|
399
|
+
// bytes; for format==1 the payload is a JFIF JPEG. Returns a dict with the
|
|
400
|
+
// header fields and the raw payload, or None.
|
|
401
|
+
py::object thumbnail(psd::PSDFile &self) {
|
|
402
|
+
const psd::ImageResourceInfo *res = findResource(self, 1036);
|
|
403
|
+
if (!res) res = findResource(self, 1033);
|
|
404
|
+
if (!res) return py::none();
|
|
405
|
+
std::string raw = resourceBytes(*res);
|
|
406
|
+
if (raw.size() < 28) return py::none();
|
|
407
|
+
auto be32 = [&](size_t o) {
|
|
408
|
+
return (uint32_t)(((uint8_t)raw[o] << 24) | ((uint8_t)raw[o+1] << 16) |
|
|
409
|
+
((uint8_t)raw[o+2] << 8) | (uint8_t)raw[o+3]);
|
|
410
|
+
};
|
|
411
|
+
auto be16 = [&](size_t o) {
|
|
412
|
+
return (uint16_t)(((uint8_t)raw[o] << 8) | (uint8_t)raw[o+1]);
|
|
413
|
+
};
|
|
414
|
+
py::dict d;
|
|
415
|
+
uint32_t fmt = be32(0);
|
|
416
|
+
d["format"] = (fmt == 1) ? "jpeg" : "raw";
|
|
417
|
+
d["width"] = be32(4);
|
|
418
|
+
d["height"] = be32(8);
|
|
419
|
+
d["bits"] = be16(24);
|
|
420
|
+
d["resource_id"] = (int)res->id; // 1036 = RGB order, 1033 = BGR order
|
|
421
|
+
d["data"] = py::bytes(raw.data() + 28, raw.size() - 28);
|
|
422
|
+
return std::move(d);
|
|
423
|
+
}
|
|
424
|
+
|
|
349
425
|
py::bytes layerImage(psd::PSDFile &self, int index, const std::string &mode) {
|
|
350
426
|
if (!self.isLoaded) throw std::runtime_error("PSD not loaded");
|
|
351
427
|
if (index < 0 || index >= (int)self.layerList.size())
|
|
@@ -537,7 +613,22 @@ PYBIND11_MODULE(psdparse, m) {
|
|
|
537
613
|
"(id, name, comment, record_*). Empty list when there are none.")
|
|
538
614
|
.def_property_readonly("color_table", &psdColorTable,
|
|
539
615
|
"Indexed-color palette as a dict (colors[], valid_count, "
|
|
540
|
-
"transparency_index), or None for non-indexed PSDs.")
|
|
616
|
+
"transparency_index), or None for non-indexed PSDs.")
|
|
617
|
+
.def_property_readonly("image_resource_ids", &imageResourceIds,
|
|
618
|
+
"List of the integer IDs of every image resource present.")
|
|
619
|
+
.def("image_resource", &imageResource, py::arg("id"),
|
|
620
|
+
"Raw bytes of the image resource with the given integer ID, or None.")
|
|
621
|
+
.def_property_readonly("icc_profile", &iccProfile,
|
|
622
|
+
"ICC profile (resource 1039) as raw bytes, or None.")
|
|
623
|
+
.def_property_readonly("exif", &exifData,
|
|
624
|
+
"EXIF block (resource 1058) as raw bytes, or None.")
|
|
625
|
+
.def_property_readonly("xmp", &xmpMetadata,
|
|
626
|
+
"XMP metadata (resource 1060) as a UTF-8 str, or None. Use "
|
|
627
|
+
"image_resource(1060) for raw bytes if not valid UTF-8.")
|
|
628
|
+
.def_property_readonly("thumbnail", &thumbnail,
|
|
629
|
+
"Embedded thumbnail (resource 1036/1033) as a dict "
|
|
630
|
+
"(format, width, height, bits, resource_id, data), or None. "
|
|
631
|
+
"For format=='jpeg', data is JFIF JPEG bytes.");
|
|
541
632
|
|
|
542
633
|
// Enum-like ints exposed as module attributes for convenience
|
|
543
634
|
m.attr("LAYER_TYPE_NORMAL") = (int)psd::LAYER_TYPE_NORMAL;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Tests for image-resource raw-bytes access (0.5.0):
|
|
2
|
+
|
|
3
|
+
PSDFile.image_resource_ids / image_resource(id) and the typed shortcuts
|
|
4
|
+
icc_profile / exif / xmp / thumbnail. EXIF and ICC bytes here were verified
|
|
5
|
+
byte-identical against psd-tools 1.17.
|
|
6
|
+
"""
|
|
7
|
+
import psdparse
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_resource_ids_are_ints(psd_group):
|
|
12
|
+
ids = psd_group.image_resource_ids
|
|
13
|
+
assert isinstance(ids, list)
|
|
14
|
+
assert all(isinstance(i, int) for i in ids)
|
|
15
|
+
assert len(ids) > 0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_generic_matches_typed(psd_group):
|
|
19
|
+
# image_resource(1058) is the same bytes as the exif shortcut.
|
|
20
|
+
assert psd_group.image_resource(1058) == psd_group.exif
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_absent_resource_is_none(psd_group):
|
|
24
|
+
# 9999 is not a real Photoshop resource id.
|
|
25
|
+
assert psd_group.image_resource(9999) is None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_exif_present(psd_group):
|
|
29
|
+
exif = psd_group.exif
|
|
30
|
+
if exif is None:
|
|
31
|
+
pytest.skip("sample has no EXIF resource")
|
|
32
|
+
assert isinstance(exif, bytes) and len(exif) > 0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_xmp_is_xml(psd_group):
|
|
36
|
+
xmp = psd_group.xmp
|
|
37
|
+
if xmp is None:
|
|
38
|
+
pytest.skip("sample has no XMP resource")
|
|
39
|
+
assert isinstance(xmp, str)
|
|
40
|
+
assert "xpacket" in xmp or "xmpmeta" in xmp
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_icc_profile(psd_text):
|
|
44
|
+
# fontsample.psd carries an ICC profile.
|
|
45
|
+
icc = psd_text.icc_profile
|
|
46
|
+
if icc is None:
|
|
47
|
+
pytest.skip("sample has no ICC profile")
|
|
48
|
+
assert isinstance(icc, bytes) and len(icc) > 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_thumbnail_header(psd_group):
|
|
52
|
+
th = psd_group.thumbnail
|
|
53
|
+
if th is None:
|
|
54
|
+
pytest.skip("sample has no thumbnail")
|
|
55
|
+
assert th["width"] > 0 and th["height"] > 0
|
|
56
|
+
assert th["format"] in ("jpeg", "raw")
|
|
57
|
+
assert isinstance(th["data"], bytes)
|
|
58
|
+
if th["format"] == "jpeg":
|
|
59
|
+
assert th["data"][:2] == b"\xff\xd8" # JFIF/JPEG SOI marker
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_thumbnail_decodes(psd_group):
|
|
63
|
+
Image = pytest.importorskip("PIL.Image")
|
|
64
|
+
th = psd_group.thumbnail
|
|
65
|
+
if th is None or th["format"] != "jpeg":
|
|
66
|
+
pytest.skip("sample has no JPEG thumbnail")
|
|
67
|
+
import io
|
|
68
|
+
img = Image.open(io.BytesIO(th["data"]))
|
|
69
|
+
assert (img.width, img.height) == (th["width"], th["height"])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|