psd-toolsの出力を調べるときは、PSD内の保存済み合成画像を取り出したのか、レイヤーから再合成したのかを分けます。
保存済み画像が取り出せても、レイヤーの表示を変更した結果や、効果を再現した結果まで確認したことにはなりません。
最初に使用する版を記録し、代表PSDで保存済み画像と再合成の出力を比較します。
基本の読み込み・PNG保存・レイヤーの取得はpsd-toolsの使い方で説明しています。
2026年10月25日に公式latest文書1.26.0を照合しましたが、比較コードの再実行や画面確認は行っていません。
以下の実測は1.21.0での過去記録で、現在の版でも同じ結果になるとは限りません。
保存済み画像と再合成の使い分け
| 呼び出し | 確認する対象 |
|---|---|
psd.topil() |
PSD内の保存済み合成画像を取り出す。画像が得られるか、寸法・色・透明度を確認する |
psd.composite() |
保存済み画像を使う場合と再合成する場合がある。結果だけで経路を判断しない |
psd.composite(ignore_preview=True) |
保存済みプレビューを使わず、レイヤーから合成する |
psd.composite(ignore_preview=True, force=True) |
再合成とベクトル描画を明示して比較する。未対応機能を再現できるという指定ではない |
10月3日に確認した1.21.0の実装では、ignore_preview、force、layer_filter の指定や、文書が更新された状態などで、保存済み画像を使う分岐が変わります。
現在のPSDImage APIと実行する版を照合します。
保存済み合成画像は、PSDを保存した時点の表示を確認するために使います。
Photoshopでの保存設定や、別のソフトで作成・変更したPSDの内容によって条件が異なるため、「プレビューがあれば必ずPhotoshopと同じ」とは扱いません。
10月3日にPythonで生成したサンプルでも、透明度の差を確認しました。
対応の表示と実際の描画を分ける
| PSDの要素 | 確認する内容 |
|---|---|
| ピクセルレイヤー・グループ | 表示状態、親グループ、マスク、描画モードと合成結果 |
| 調整レイヤー | 実行版の対応種類、色モード、パラメーターと見た目 |
| シェイプ・塗り・効果 | 追加依存関係の有無と、対応している描画の範囲 |
| 文字 | 保存された画素の取得と、文字・フォントの描き直しを区別する |
| スマートオブジェクト | レイヤーの保存画素と、埋め込み・リンク先データの取得を区別する |
| CMYK・16bit・32bit | 同じ条件の代表PSDで、色と出力の対応を個別に確認する |
公式のFeatures一覧には非対応と書かれた項目があり、Usage・合成APIでは一部の調整レイヤーへの対応も案内されています。
2026年10月3日に読んだ文書間でも説明の粒度が一致しないため、一つの表だけで合成可否を決めません。
公式Features、Usage、合成APIと実際の出力を照合します。
10月3日の実行環境の1.21.0には、明るさ・コントラスト、レベル、カーブ、露光量、反転、ポスタリゼーション、2階調化、色相・彩度の処理が登録されていました。
処理が存在することと、そのPSDの色モード・設定でPhotoshopと一致することは別の確認です。
このうち色相・彩度と反転、登録に無い白黒の3つを、後の節「Photoshopで作ったPSDで確かめた結果」で比べました。
ほかの調整レイヤーは、Photoshopと比べる試験をしていません。
追加パッケージで解決する範囲
ベクトル描画などに必要な依存関係を要求された場合は、実行中のPython環境へ composite の追加依存関係を入れます。
python -m pip install "psd-tools[composite]"
10月25日に確認した公式1.26.0文書では、追加依存関係としてaggdrawとscipyを案内しています。
scikit-imageは1.24.0からcompositeの追加依存関係ではなくなりました。
これらを導入して読み込みエラーが消えても、未対応のレイヤー効果やフォント描画がすべて再現されるわけではありません。
エラー文、Pythonとライブラリの版、対象PSDを記録し、出力画像も確認します。
変更履歴には描画・レイヤー状態の修正も含まれます。
アップデート前後は同じPSDを別の出力先で比較し、以前の結果を上書きせず残します。
保存済み画像と再合成を比較するコード
次のコードを compare_psd.py として保存します。
入力PSDは書き換えず、新しい出力フォルダーに条件・結果のJSONと、取得できた画像を保存します。
保存済み画像を取得できない場合などは、PNGが2枚そろわないことがあります。
既にあるフォルダーを指定した場合は処理を止めます。
"""Compare cached preview and recomposition; never edit input or follow external links."""
import argparse
import hashlib
import json
import platform
from importlib.metadata import version
from pathlib import Path
import numpy as np
from PIL import Image
from psd_tools import PSDImage
def compare(source, output):
source = Path(source)
output = Path(output)
digest = hashlib.sha256(source.read_bytes()).hexdigest()
psd = PSDImage.open(source)
output.mkdir(parents=True, exist_ok=False)
report = {
"source_sha256": digest,
"python": platform.python_version(),
"psd_tools": version("psd-tools"),
"pillow": version("Pillow"),
"numpy": version("numpy"),
"size": list(psd.size),
"has_preview": psd.has_preview(),
"layers": [],
"results": {},
}
for layer in psd.descendants():
report["layers"].append({
"name": layer.name, "kind": layer.kind,
"visible": layer.is_visible(), "has_effects": layer.has_effects(),
})
images = {}
operations = {
"preview": lambda: psd.topil(apply_icc=True),
"recomposed": lambda: psd.composite(ignore_preview=True, force=True, apply_icc=True),
}
for name, operation in operations.items():
try:
image = operation()
if image is None:
report["results"][name] = {"status": "no_image"}
continue
image.save(output / (name + ".png"))
images[name] = image.convert("RGBA")
report["results"][name] = {"status": "saved", "mode": image.mode, "size": list(image.size)}
except Exception as error:
report["results"][name] = {"status": "error", "type": type(error).__name__, "message": str(error)}
if len(images) == 2:
a, b = images["preview"], images["recomposed"]
if a.size != b.size:
report["comparison"] = {"status": "size_mismatch"}
else:
def on_white(image):
bg = Image.new("RGBA", image.size, (255, 255, 255, 255))
return np.asarray(Image.alpha_composite(bg, image).convert("RGB"), dtype=np.int16)
delta = np.abs(on_white(a) - on_white(b))
alpha_delta = np.abs(np.asarray(a.getchannel("A"), dtype=np.int16) -
np.asarray(b.getchannel("A"), dtype=np.int16))
report["comparison"] = {
"status": "compared", "background": "white",
"rgb_mae_0_255": float(delta.mean()), "rgb_max_0_255": int(delta.max()),
"rgb_changed_pixel_percent": float(np.any(delta != 0, axis=2).mean() * 100),
"alpha_mae_0_255": float(alpha_delta.mean()), "alpha_max_0_255": int(alpha_delta.max()),
}
report["source_unchanged"] = hashlib.sha256(source.read_bytes()).hexdigest() == digest
(output / "report.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
return report
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("input_psd")
parser.add_argument("new_output_folder")
args = parser.parse_args()
report = compare(args.input_psd, args.new_output_folder)
print(json.dumps(report.get("comparison", {"status": "not_compared"}), ensure_ascii=True))
python compare_psd.py sample.psd compare-output-01
report.jsonで成否を確認する
実行が終わったら、出力フォルダーのreport.jsonをテキストエディタで開きます。
画像処理の例外はコード内で捕捉しているため、終了コード0だけでは両画像の出力成功を判断できません。
results.preview.statusとresults.recomposed.statusが両方savedか確認します。no_imageなら画像が得られず、errorなら同じ項目のtypeとmessageで原因を調べます。comparison.statusがcomparedのときにだけ、RGBとアルファの差を読みます。source_unchangedがtrueか確認し、比較前後で入力PSDの内容が変わっていないか確かめます。
size_mismatchは寸法が違うため数値比較をしていない状態です。
画面にnot_comparedと出たら、平均差0と読み替えず、JSON内の両方のresultsを確認してください。
PSDを開く段階や出力先の作成で失敗した場合などは、JSON自体が作られないこともあります。
その場合はターミナルのエラーを読み、入力パス・書き込み権限・出力フォルダーの重複を確認します。
report.json の results は、画像の保存・画像なし・処理エラーを区別します。
両画像の寸法が同じ場合だけ、白背景に重ねたRGBの平均絶対差・最大差・差のある画素割合と、アルファの平均差・最大差を記録します。
透明部分のRGBだけを比較すると、見えない画素の色が差として数えられるため、背景と透明度を分けました。
平均差0は、この比較条件で同じ画素値だったという意味です。
Photoshopの表示との一致や、色管理を含む納品条件を保証する数値ではありません。
平均だけでは小さい領域の差を見落とすため、透明部分、文字、影、境界線などの箇所も画像で確認します。
寸法が異なる場合は画像を勝手に拡大・縮小せず、原因を調べます。
2026年10月3日のサンプルで確認した結果
2026年10月3日にWindows 11、Python 3.13.13、psd-tools 1.21.0、Pillow 12.3.0、NumPy 2.5.3でコードを実行しました。
入力は当方がPythonで作成したRGB・8bitのサンプルです。
Photoshopで作成したPSDやPhotoshopのスクリーンショットではありません。
| 入力 | RGB平均差 | α最大差 |
|---|---|---|
| 単色(32×24px) | 0 | 0 |
| 透明な周囲・非表示グループ(640×480px) | 約221.80 | 255 |
RGBの平均差は白背景に重ねた画像の値、αの最大差はアルファの値です。
単色サンプルはこの条件で一致し、透明な周囲を持つサンプルは保存済み画像が不透明・再合成が透明で差が出ました。
この2例は、同じ比較コードが一致と差を区別できるかを確認したものです。
入力ハッシュの不変、既存フォルダーへの処理拒否、既存レポートの不変を含む6項目を確認しました。
実画像は入門記事の実行結果にも掲載しています。
この2例には、文字・調整レイヤー・マスクや効果・スマートオブジェクト・16/32bit・Photoshopとの表示一致を含めていません。
調整レイヤー・マスク・スマートオブジェクト・効果は、次の節でPhotoshopで作ったPSDを使って確かめました。
記事全体の対応表を、これらの例だけで検証したとはしていません。
Photoshopで作ったPSDで確かめた結果
2026年10月17日に、Adobe Photoshop 2026(27.11.0)で練習用のPSDを作り、上の compare_psd.py を書き換えずに実行しました。
環境はWindows 11 Pro(10.0.26200)、Python 3.13.13、psd-tools 1.21.0、Pillow 12.3.0、NumPy 2.5.3です。
PSDは800×600px・RGB・8bit・白い背景で、左の色の帯に、帯ごとに選択範囲から作った調整レイヤー(色相・彩度で色相+90、階調の反転、白黒)を重ねました。
右には、レイヤーマスクで左半分を隠したオレンジの四角、埋め込みのスマートオブジェクト、境界線(外側8px)とベベルとエンボスを付けた水色の四角を置いています。
PSDの作成と、比べる基準にしたPhotoshopの画像(PNGのコピー保存)はスクリプトで準備したため、作成の手順は確認に含めていません。
保存時の互換性の設定([ファイル管理]の「PSD および PSB ファイルの互換性を優先」)は「常にオン」です。

Photoshopで保存したPNGと、掲載コードが書き出した2枚を、上と同じく白背景に重ねたRGBの差(0〜255)で部分ごとに比べました。
| 部分 | 保存済み画像(preview) | 再合成(recomposed) |
|---|---|---|
| 色相・彩度(色相+90) | 差0 | 最大差1 |
| 階調の反転 | 差0 | 最大差1 |
| 白黒 | 差0 | 最大差132。元の色のまま |
| 調整なしの帯 | 差0 | 差0 |
| レイヤーマスク | 差0 | 差0 |
| スマートオブジェクト(埋め込み) | 差0 | 差0 |
| 境界線(外側8px) | 差0 | 角の丸みの20画素で差(最大58) |
| ベベルとエンボス | 差0 | 最大差150。明暗が付かない |
保存済み画像はPhotoshopで保存したPNGと全画素で一致し、白黒の調整レイヤーとベベルとエンボスも反映されていました。
再合成では、白黒の帯が元の色のまま残り、ベベルとエンボスの明暗が付きませんでした。
実行時に警告やエラーは出ず、画面に出た結果は全体の値だけです。
全体の値は rgb_mae_0_255 が約7.18、rgb_max_0_255 が150、rgb_changed_pixel_percent が28.575、アルファの差は0でした。
色相・彩度と反転の差は1以内で、Photoshopで開いて見比べても違いは分かりませんでした。
境界線は再合成でも描かれ、差は4つの角の丸みの部分だけでした。

report.json の layers では、白黒の調整レイヤーの kind は blackandwhite と読めていました。
種類が読めることと、再合成に反映されることは別に確かめる必要があります。
効果を付けたレイヤーの has_effects は true でしたが、どの効果が再現されたかは画像を見ないと分かりません。
互換性を優先しない設定で保存した複製
同じ文書を、互換性を優先しない設定に切り替えて保存した複製でも実行しました(切り替えと保存はスクリプトで行い、保存後に設定を「常にオン」へ戻しました)。
has_preview は false になり、results の preview は no_image でした。
比較は行われず、画面に出た結果は {"status": "not_compared"} だけです。
再合成の画像は、互換性を優先した版の再合成と全画素で一致し、白黒とベベルとエンボスが反映されないままでした。
この練習用PSDでは、互換性を優先しないで保存すると、Photoshopの見た目どおりの画像をこのコードでは得られませんでした。
同じ出力先を指定した2回目の実行は FileExistsError で止まり、2つのPSDのハッシュはどの実行の後も変わりませんでした。
ほかの調整レイヤー(カーブ・レベルなど)や別の設定値、ベベルとエンボス以外の効果、リンクしたスマートオブジェクト、文字、CMYK・16/32bit、追加パッケージを入れた後の結果、ほかの変換ツールとの比較は確かめていません。
スマートオブジェクトの表示と中身
layer.topil() などで保存画素を取り出す処理と、layer.smart_object から中身のデータを読む処理は異なります。
埋め込みならPSD内にデータがあり、リンクなら外部ファイルが必要になる場合があります。
保存画素が取り出せるだけでは、リンク先を読めたことにはなりません。
現在のSmartObject APIは、リンク先を読むディレクトリの指定も案内しています。
引数とパスの扱いは版で異なるため、今回の1.21.0とlatest文書を同じ条件として転用しないでください。
今回の比較コードはリンク先を開きません。
旧167ファイルの検証記録と10月の結果
旧記事には、2026年9月29日に1.21.0で公式リポジトリのPSD167個を試し、プレビューなし9個、シェイプを含む7個の依存関係エラーなどを確認したという記録がありました。
以下の測定表と出力例は、その過去の掲載内容を保持した資料です。
167ファイル分の元ログ・入力ハッシュ・条件は未取得のため、同じ試験を再実行した結果や、現在の版全体の成功率として使っていません。
過去の差の平均値と、10月3日・17日の白背景RGB・アルファを分けた指標も同一の測定条件とはしていません。
過去の測定表とスマートオブジェクトの記録を表示(今回の再試験ではありません)
| PSD の中身 | 色の差の平均 | 見た目 |
|---|---|---|
| トーンカーブ(RGB) | 0.77 | ほぼ同じ |
| レベル補正(RGB) | 0.69 | ほぼ同じ |
| 色相・彩度(RGB) | 0.87 | ほぼ同じ |
| 明るさ・コントラスト(RGB) | 3.08 | 少し違う |
| 2階調化(RGB) | 0.00 | 同じ |
| 2階調化(CMYK) | 68.73 | 大きく違う |
| 調整レイヤー16種類+グラデーション・パターンの効果 | 20.92 | 大きく違う |
| 文字に10種類のレイヤー効果(ドロップシャドウ・光彩・ベベルなど) | 7.38 | 違う |
| シェイプ+グラデーションオーバーレイ・境界線 | 13.40 | 違う |
| 境界線(シェイプなし) | 0.00〜0.21 | ほぼ同じ |
| レイヤー | 種類(smart_object.kind) | 中身を読めたか |
|---|---|---|
| embedded-png | data(埋め込み) | 読めた(17,272 バイト) |
| linked-png | external(リンク) | FileNotFoundError |
| linked-psd | external(リンク) | FileNotFoundError |
Photoshopへ戻して確認する場合
合成画像を取得できない、必要な効果が欠ける、代表PSDで見た目が変わる場合は、Photoshopで開いて確認します。
元PSDを保存し直す必要があるときは複製を使い、互換性に関する保存設定と、保存後の合成画像の有無を確認してください。
互換性の設定だけで、すべての色・透明度・他ソフトでの描画を保証するわけではありません。
上の練習用PSDでは、互換性を優先しない設定で保存すると保存済み画像を取り出せず、再合成には白黒とベベルとエンボスが反映されませんでした。
PSDごとの合成画像をPhotoshopで出力する手順はフォルダー内PSDのJPG・PNG出力へ、外部CLIで保存済み合成画像を変換する手順はImageMagickの記事へ進めます。
目的に合わせて処理を選び、同じ代表PSDで出力を照合します。
旧掲載コードの比較資料
旧コード・コマンド・出力例の原文です。
今回の実行結果と区別して参照してください。
旧コード5ブロックを表示(原文・全例の現行版動作は未検証)
ImportError: Vector shape rendering requires: aggdrawpip install "psd-tools[composite]"import sys
from psd_tools import PSDImage
# psd-tools 1.21.0 が合成できる調整レイヤー(README の7種類+色相・彩度)
OK_ADJ = {"brightnesscontrast", "levels", "curves", "exposure", "invert", "posterize", "threshold", "huesaturation"}
ADJ = {"brightnesscontrast","levels","curves","exposure","invert","posterize","threshold","huesaturation","colorbalance",
"blackandwhite","photofilter","channelmixer","colorlookup","gradientmap","selectivecolor","vibrance"}
FILL_FX = {"ColorOverlay", "GradientOverlay", "PatternOverlay"}
def inspect(path):
psd = PSDImage.open(path)
print(f"■ {path} {psd.width}x{psd.height} プレビュー画像: {'あり' if psd.has_preview() else 'なし'}")
for layer in psd.descendants():
notes = []
if layer.kind in ADJ and layer.kind not in OK_ADJ:
notes.append(f"調整レイヤー {layer.kind}(合成は未対応)")
if layer.kind == "type":
notes.append("文字レイヤー(フォントの描画は未対応)")
if layer.kind == "smartobject":
notes.append("スマートオブジェクト(中身の編集は未対応)")
if layer.kind == "shape":
notes.append("シェイプ(描くには composite の追加パッケージが要る)")
if layer.has_effects():
fx = [e.__class__.__name__ for e in layer.effects if e.enabled]
other = [f for f in fx if f not in FILL_FX]
if other:
notes.append("レイヤー効果 " + ",".join(other) + "(多くは合成が未対応)")
if notes:
print(f" - {layer.name!r}: " + " / ".join(notes))
for p in sys.argv[1:]:
inspect(p)python inspect_psd.py design.psd■ repo/tests/psd_files/fill_adjustments.psd 256x256 プレビュー画像: あり
- 'Rectangle 1': シェイプ(描くには composite の追加パッケージが要る)
- 'Vibrance 1': 調整レイヤー vibrance(合成は未対応)
- 'Color Balance 1': 調整レイヤー colorbalance(合成は未対応)
- 'Black & White 1': 調整レイヤー blackandwhite(合成は未対応)
- 'Gradient Map 1': 調整レイヤー gradientmap(合成は未対応)
■ repo/tests/psd_files/text.psd 400x400 プレビュー画像: あり
- 'Line 1 Line 2 Line 3 and text': 文字レイヤー(フォントの描画は未対応)