Amazonで商品を見るセール会場へ

PythonでPSDをPNGに変換する方法|psd-toolsの使い方と合成の限界

psd-tools は、PythonでPSDのレイヤー情報を読み取り、画像を出力するためのライブラリです。Photoshopを起動せずに処理できますが、Photoshopの描画結果をすべて再現するものではありません。見本画像の変換、レイヤーの検査、単純なピクセルレイヤーの処理に分けて使います。

PSDをPhotoshop自身で開いて変換する方法は、フォルダー内のPSDをJSXで書き出す記事に分けています。

2026年10月17日に、Adobe Photoshop 2026(27.11.0)で作ったPSDを使い、この記事の掲載コードを psd-tools 1.21.0 でそのまま動かしました。

書き出したPNGはPhotoshopで開いて、元のPSDと見比べています。

結果は「Photoshopで作ったPSDで確かめた結果」の節にまとめました。

目次

psd-toolsで扱えること・Photoshopで確認すること

目的 方法と確認点
PSD全体をPNGにする PSDImage.open() と composite()。保存済みプレビューを使う場合と、レイヤーから合成する場合を区別します。
レイヤーを検査する 名前・種類・可視性・位置などを読む。グループ内も読む場合は descendants() を使います。
ピクセルレイヤーを取り出す layer.composite()。単独レイヤーの出力と、文書全体の見た目は異なります。
名前や可視性を変更してPSDを保存する 対応する属性を変更し、別名で保存して読み直します。
文字内容の差し替え・複雑な表現の再現 APIの対応範囲を確認します。テキスト情報の読み取りと、文字の変更・フォントによる描き直しは別です。

公式のUsageは、合成結果がPhotoshopと異なる場合があること、調整レイヤーやレイヤー効果の対応に制約があることを説明しています。納品用の画像はPhotoshopの書き出し結果と照合してください。

インストールと使用する版の記録

Pythonの作業環境にインストールします。複数のPythonがある場合は、実行するPythonからpipを呼び出します。

python -m pip install psd-tools

上の行はPythonコードではなく、ターミナルで実行するコマンドです。使用する版は次のPythonコードで記録できます。

from importlib.metadata import version
print(version("psd-tools"))
print(version("Pillow"))

ベクターや一部の効果を合成するために追加の依存関係が必要になった場合は、公式文書の psd-tools[composite] を確認します。追加の依存関係を入れても、すべてのPhotoshop機能が再現されるわけではありません。

PSD全体を1枚のPNGとして保存する

次の例は sample.psd の合成画像を、新しい output.png へ保存します。既存ファイルがある場合は FileExistsError で止まり、上書きしません。

from pathlib import Path
from psd_tools import PSDImage

psd = PSDImage.open("sample.psd")
image = psd.composite()
with Path("output.png").open("xb") as stream:
    image.save(stream, format="PNG")

composite() は、条件によって保存済みの合成プレビューを利用します。プレビューを使わず、ライブラリでレイヤーを合成する場合は次のように指定します。PSDImage API

image = psd.composite(ignore_preview=True)

この指定は「Photoshopと同じ結果にする」という意味ではありません。保存済み画像を読む処理から、psd-toolsでレイヤーを合成する処理へ切り替える指定です。透明度や効果が変わる場合は、両方の画像を比較して原因を調べます。

Photoshopで作ったPSDでは、ignore_preview=True を付けるとドロップシャドウが消えました。

指定なしの composite() は保存済みの合成画像を使い、影も含めてPhotoshopの結果とほぼ一致しました(違いは文字の縁のわずかな色だけ)。

レイヤー効果を使ったPSDでは、指定なしの結果を先に確かめてから、再合成に切り替えるかを決めます。

グループ内のレイヤーと表示状態を調べる

文書をそのまま反復すると、最上位のレイヤーとグループを取得します。階層の内側まで調べる場合は descendants() を使います。

for layer in psd.descendants():
    print(layer.name, layer.kind, layer.bbox,
          layer.visible, layer.is_visible())

visible はそのレイヤー自身の設定です。is_visible() は親グループの状態も反映します。今回の自作サンプルでは、青いレイヤー自身を表示にしても、親グループを非表示にすると visible は真、is_visible() は偽になりました。表示対象を抽出する場合、この違いを確認してください。

Photoshopで作ったPSDでも同じで、非表示のグループに入れた青いレイヤーは visible が真、is_visible() が偽でした。

Photoshopのレイヤーパネルでも、このレイヤーの目のアイコンは付いたままで、グループの目だけが消えています。

ドロップシャドウを付けたレイヤーの bbox は (380, 140, 580, 340) で、影の範囲は含みませんでした。

Photoshopのスクリプトで同じレイヤーの範囲を読むと、影を含めて左375・上140・右605・下372 pxでした。

表示されるピクセルレイヤーを個別に保存する

次の例は、表示対象のピクセルレイヤーだけを出力します。連番を付け、ファイル名に使わない記号を置き換えます。シェイプ、文字、スマートオブジェクトをこの例だけで完全に変換するとはしていません。

from pathlib import Path

target = Path("output_layers")
target.mkdir(parents=True, exist_ok=True)
for index, layer in enumerate(psd.descendants()):
    if layer.kind != "pixel" or not layer.is_visible():
        continue
    image = layer.composite()
    if image is None:
        continue
    safe = "".join(c if c.isalnum() or c in "-_" else "_"
                   for c in layer.name)[:80] or "layer"
    file = target / f"{index:03d}_{safe}.png"
    with file.open("xb") as stream:
        image.save(stream, format="PNG")

レイヤー単体の画像サイズと、PSDのキャンバス上の位置は分けて扱います。配置も再現する場合は bbox の座標を残してください。Photoshop自身でレイヤーを書き出す手順は、レイヤーを個別ファイルに書き出す記事を参照してください。

Photoshopで作ったPSDでは、000_red_layer__1.png と 003_green-shadow.png の2枚が出力されました。

番号はグループや非表示のレイヤーも数えるので、001と002は欠番になります。

レイヤー名の「red layer #1」は、空白と「#」が「_」に置き換わりました。

文字レイヤーは種類が type なので、この例では出力されません。

ドロップシャドウを付けたレイヤーのPNGは200×200 pxで、影は入っていませんでした。

属性の変更は別名で保存する

名前の変更は次のように行えます。保存先には未使用のファイル名を指定し、保存後にPSDを開き直して変更を確認します。

psd[0].name = "renamed-card"
psd.save("renamed.psd")
check = PSDImage.open("renamed.psd")
print(check[0].name)

レイヤー構造や属性の変更により、保存時の合成プレビューが作り直される場合があります。元のPhotoshop表示を維持できるかは別途確認してください。低レベルのレコード操作、文字内容の編集、リンクされたスマートオブジェクトの置き換えは、この例の確認対象に含めていません。

psd[0] は、Photoshopのレイヤーパネルで一番下のレイヤーを指します。

Photoshopで作ったPSDでは、一番下の赤いレイヤーの名前が「renamed-card」に変わりました。

一番上のレイヤーを変えたいときは、psd[-1] のように後ろから数えます。

保存した renamed.psd はPhotoshopで開けて、文字レイヤーとドロップシャドウも残っていました。

psd-toolsで読んだ保存済みの合成画像も、元のPSDと同じでした。

psd-toolsでpsd[0]の名前を変えて保存したPSDをPhotoshopで開き、一番下のレイヤーがrenamed-cardになった画面
2026-10-17撮影。Adobe Photoshop 2026 (27.11.0)、Windows 11 Pro (10.0.26200)。掲載コードで psd[0] の名前を変えて保存した renamed.psd をPhotoshopで開いた実機画面。一番下の赤いレイヤーが renamed-card になり、文字レイヤーとドロップシャドウは残っている。

今回の実行結果と未確認の範囲

2026年10月3日にWindows上のPythonで、psd-tools 1.21.0、Pillow 12.3.0、NumPy 2.5.3を使用しました。自作の640×480px、8bitのサンプルに赤いピクセルレイヤーと、非表示グループ内の青いピクセルレイヤーを作成して確認しています。

確認したのはPSDの作成・保存・再読み込み、可視性の取得、画像出力、保存名の記号置換、既存PNGへの書き込み拒否、名前変更後の再読み込みです。出力画像の寸法と画素も確認しました。下の画像はPythonで出力した検証結果で、Photoshopのスクリーンショットではありません。

今回の生成PSDでは、保存済みプレビューから得た画像と、ignore_preview=True で得た画像に透明度の差がありました。一般のPSDすべてで同じ差が出るとはしていません。処理対象のPSDで透明度を確認する必要があります。

公式サイトのlatest文書は取得時に1.23.0を表示しており、今回の実行環境とは版が異なりました。文書にある group.create_pixel_layer() は今回の環境では使用できず、文書側でレイヤーを作成してグループへ追加しました。コードを転用するときは、文書の版とインストールした版を照合してください。

10月3日の確認では、文字、調整レイヤー、複雑なマスク・効果、16bit・32bit、Photoshopの描画結果との一致は確かめていませんでした。

文字1つとドロップシャドウ1つは、10月17日にPhotoshopで作ったPSDで確かめました(下の節)。

調整レイヤー、複雑なマスク、ほかの効果、16bit・32bitは、まだ確かめていません。

これらを含むファイルは、代表サンプルで差を確認してから一括処理へ進みます。

psd-toolsで保存済みプレビューから出力した検証画像。生成サンプルでは背景が不透明なRGBになった。
psd-toolsで保存済みプレビューから出力した検証画像。生成サンプルでは背景が不透明なRGBになった。 Pythonで生成した実際の出力画像(2026年10月3日)。
psd-toolsでignore_preview=Trueを指定して出力した検証画像。赤いレイヤーだけが表示され、周囲は透明。
psd-toolsでignore_preview=Trueを指定して出力した検証画像。赤いレイヤーだけが表示され、周囲は透明。 Pythonで生成した実際の出力画像(2026年10月3日)。

Photoshopで作ったPSDで確かめた結果

2026年10月17日に、Adobe Photoshop 2026(27.11.0)とWindows 11 Pro(10.0.26200)で確かめました。

Pythonは3.13.13で、psd-tools 1.21.0、Pillow 12.3.0、NumPy 2.5.3を使っています。

PSDは、Photoshopで640×480 px・RGB・8 bitの透明な文書を作り、4つの要素を入れて保存しました。

上から順に、文字レイヤー、ドロップシャドウを付けた緑の四角、非表示のグループとその中の青い四角、赤い四角です。

PSDの作成と保存はPhotoshopのスクリプトで行ったので、作り方の手順は確認に含めていません。

保存したときの[環境設定]→[ファイル管理]の[PSD および PSB ファイルの互換性を優先]は「常にオン」でした。

Photoshopで作った練習用PSD。文字レイヤー、ドロップシャドウ付きの緑の四角、非表示グループ内の青い四角、赤い四角がレイヤーパネルに並んだ画面
2026-10-17撮影。Adobe Photoshop 2026 (27.11.0)、Windows 11 Pro (10.0.26200)。psd-toolsで読む前の練習用PSDをPhotoshopで開いた実機画面。非表示のグループ(hidden-group)の中に青いレイヤーがあり、緑の四角にはドロップシャドウが付いている。

記事の掲載コードは変えずに、上から順に同じPythonで実行しました。

ignore_preview=True の行は画像を保存しないので、見比べるために別名のPNGへ保存する処理だけを確認用に足しています。

掲載コードは、psd-tools 1.21.0 でエラーなく動きました。

比べる基準には、同じPSDをPhotoshopでPNGとして保存した画像を使いました(この保存もスクリプトで実行)。

書き出したPNGは、Photoshopの[ファイル]→[開く]で開いて見比べました。

確かめたこと結果
指定なしの composite()保存済みの合成画像が使われた。ドロップシャドウも入り、Photoshopで保存したPNGと透明度は全画素で一致。色の差は文字の縁の81画素で、最大5(0〜255の段階)
ignore_preview=Trueドロップシャドウが消えた。透明度が違う画素は11,005画素で、すべて影の範囲。赤・緑の四角と文字は一致
Photoshopで開いたときの範囲指定なしは544×311 px、ignore_preview=True は520×280 px(属性パネルのW・H)
非表示グループの中のレイヤーvisible は真、is_visible() は偽。どちらのPNGにも入らない
レイヤーごとの書き出し赤と緑の2枚。どちらも200×200 pxで影なし。文字レイヤーは対象外
同じフォルダーで2回目の実行FileExistsError で止まり、output.png は書き換わらない
名前の変更psd[0](一番下の赤)が renamed-card になり、Photoshopで開けた
psd-toolsのcomposite()を指定なしで書き出したPNGをPhotoshopで開いた画面。緑の四角のドロップシャドウが残っている
2026-10-17撮影。Adobe Photoshop 2026 (27.11.0)、Windows 11 Pro (10.0.26200)。指定なしの composite() で書き出した output.png をPhotoshopで開いた実機画面。ドロップシャドウが入り、属性パネルの範囲はW 544 px・H 311 px。
psd-toolsのcomposite(ignore_preview=True)で書き出したPNGをPhotoshopで開いた画面。緑の四角のドロップシャドウが消えている
2026-10-17撮影。Adobe Photoshop 2026 (27.11.0)、Windows 11 Pro (10.0.26200)。ignore_preview=True で再合成したPNGをPhotoshopで開いた実機画面。ドロップシャドウが無くなり、属性パネルの範囲はW 520 px・H 280 px。

指定なしで書き出したPNGをPhotoshopで開くと、題名に「RGB/8#」と出て、カラープロファイルが付いていませんでした。

ignore_preview=True とレイヤーごとのPNGには、sRGBのプロファイルが付いていました。

色の管理が要る納品では、プロファイルの有無も確かめてください。

互換性を優先しない設定で保存したPSDで、指定なしの結果がどうなるかは試していません。

調整レイヤー、マスク、スマートオブジェクト、ほかのレイヤー効果、16 bit・32 bit、CMYK、Macも試していません。

エラーと見た目の差を調べる

症状 調べる内容
ModuleNotFoundError 実行するPythonと、pipで入れた環境が同じか
文書のメソッドが存在しない インストールした版と、参照している文書の版
高度な合成の依存関係を要求される 公式文書のcomposite追加依存関係と対象レイヤー
非表示グループの子が出力対象になる visible と is_visible() の取り違え
透明度や色、効果が変わる プレビュー利用・再合成の違い、ICC、レイヤーの対応範囲
レイヤー画像の保存で止まる 画像がNoneか、保存名が重複していないか、出力先の書き込み権限

旧記事の広いAPI例は比較資料として末尾へ残しています。現在の版で全例を検証したものではなく、先に記した実行範囲と区別してください。記事に対応するコード一覧、公式Usage、プロジェクトのGitHub、PyPIから版と仕様を確認できます。

旧掲載コードの比較資料

旧API例には現在の版で未確認の操作や必要条件を省略した断片があります。検証済みのコード一覧ではありません。

旧コード41ブロックを表示(原文・現行版の一括動作は未検証)
pip install psd-tools
pip install 'psd-tools[composite]'
# Ubuntu/Debian
apt-get install liblcms2-2

# macOS
brew install little-cms2
from psd_tools import PSDImage

# PSDファイルを開く
psd = PSDImage.open('example.psd')

# ファイル情報を表示
print(psd)  # PSDImage(mode=RGB size=101x55 depth=8 channels=3)
# 全レイヤーを合成してPNG出力
image = psd.composite()
image.save('output.png')
# 全レイヤーをループ
for layer in psd:
    print(f"レイヤー名: {layer.name}")
    print(f"レイヤータイプ: {layer.kind}")
    print(f"サイズ: {layer.width}x{layer.height}")
    print(f"可視性: {layer.visible}")
    print("---")
# 最初のレイヤーを取得
first_layer = psd[0]

# ネストしたレイヤーにアクセス
nested_layer = psd[0][0]
# ピクセルレイヤーの確認
if layer.kind == 'pixel':
    # レイヤーを画像として出力
    layer_image = layer.composite()
    layer_image.save(f'{layer.name}.png')
    
    # NumPy配列として取得
    pixels = layer.numpy()
    print(pixels.shape)  # (height, width, channels)
    
    # PIL Imageとして取得
    pil_image = layer.topil()
# グループ内のレイヤーを反復処理
for layer in psd:
    if layer.is_group():
        print(f"グループ: {layer.name}")
        for child in layer:
            print(f"  - {child.name}")
if layer.kind == 'type':
    # テキスト内容を取得
    print(f"テキスト: {layer.text}")
    
    # スタイル情報(読み取り専用)
    print(f"エンジン辞書: {layer.engine_dict}")
    print(f"リソース辞書: {layer.resource_dict}")
if layer.kind == 'shape':
    # ベクターマスク情報
    print(layer.vector_mask)
    
    # ライブシェイププロパティ
    for shape in layer.origination:
        print(shape)
import io
from PIL import Image

if layer.kind == 'smartobject':
    # スマートオブジェクトデータにアクセス
    smart_obj = layer.smart_object
    
    if smart_obj.filetype in ('jpg', 'png'):
        image = Image.open(io.BytesIO(smart_obj.data))
        image.save(f'{layer.name}_embedded.{smart_obj.filetype}')
# SolidColorFill(ベタ塗り)
# PatternFill(パターン)
# GradientFill(グラデーション)
# レイヤー情報の取得
print(f"名前: {layer.name}")
print(f"可視性: {layer.visible}")
print(f"不透明度: {layer.opacity}")  # 0-255
print(f"位置: ({layer.left}, {layer.top})")
print(f"サイズ: {layer.width}x{layer.height}")
print(f"バウンディングボックス: {layer.bbox}")  # (left, top, right, bottom)

# レイヤー属性の編集(書き込み可能)
layer.name = "新しい名前"
layer.visible = False
layer.opacity = 128
layer.left = 100
layer.top = 50
from psd_tools.constants import BlendMode

# ブレンドモードの確認
print(layer.blend_mode)

# ブレンドモードの変更
if layer.blend_mode == BlendMode.NORMAL:
    layer.blend_mode = BlendMode.SCREEN
from psd_tools.constants import ProtectedFlags

# レイヤーのピクセルと位置をロック
layer.lock(ProtectedFlags.COMPOSITE | ProtectedFlags.POSITION)
# マスクの有無を確認
if layer.has_mask():
    # マスクを取得
    mask = layer.mask
    
    # マスクを画像として出力
    mask_image = mask.topil()
    mask_image.save('mask.png')

# ベクターマスクの確認
if layer.has_vector_mask():
    print(layer.vector_mask)
# 名前でレイヤーを検索
def find_layer_by_name(psd, name):
    for layer in psd.descendants():
        if layer.name == name:
            return layer
    return None

target_layer = find_layer_by_name(psd, "背景")
# レイヤーを上に移動
layer.move_up()

# レイヤーを下に移動
layer.move_down()
# レイヤーをグループに追加
group = psd[0]
group.append(layer)

# レイヤーを削除
layer.remove()

# グループをクリア
group.clear()
from PIL import Image
from psd_tools import PSDImage

# 新しいPSDを作成
psd = PSDImage.new(mode='RGB', size=(640, 480), depth=8)

# PIL画像からレイヤーを作成
pil_image = Image.open('input.png')
layer = psd.create_pixel_layer(
    pil_image,
    name="Layer 1",
    top=0,
    left=0,
    opacity=255
)

# グループを作成
group = psd.create_group(name="Group 1")
group.append(layer)

# 保存
psd.save('new_image.psd')
# 複数のレイヤーを新しいグループにまとめる
layer1 = psd[0]
layer2 = psd[1]

group = psd.create_group(
    layer_list=[layer1, layer2],
    name="New Group"
)
# PSD全体を合成
image = psd.composite()
image.save('output.png')

# 特定のビューポートで合成
viewport = (0, 0, 300, 300)  # (x1, y1, x2, y2)
image = psd.composite(viewport=viewport)
# テキストレイヤーを除外して合成
image = psd.composite(
    layer_filter=lambda layer: layer.is_visible() and layer.kind != 'type'
)
for layer in psd:
    if layer.kind == 'pixel':
        # レイヤーを合成して出力(マスクとクリッピングレイヤーを含む)
        layer_image = layer.composite()
        layer_image.save(f'{layer.name}.png')
# レイヤーとマスクを別々にエクスポート(合成なし)
layer_image = layer.topil()
layer_image.save('layer_no_mask.png')

if layer.has_mask():
    mask_image = layer.mask.topil()
    mask_image.save('mask.png')
from psd_tools.constants import ChannelID

# RGB個別チャンネル
red = layer.topil(ChannelID.CHANNEL_0)
green = layer.topil(ChannelID.CHANNEL_1)
blue = layer.topil(ChannelID.CHANNEL_2)

# アルファチャンネル
alpha = layer.topil(ChannelID.TRANSPARENCY_MASK)
import numpy as np

# PSD全体をNumPy配列として取得
array = psd.numpy()
print(array.shape)  # (height, width, channels)

# レイヤーをNumPy配列として取得
layer_array = layer.numpy()

# 特定のチャンネルのみ取得
color_only = layer.numpy(channel='color')
alpha_only = layer.numpy(channel='alpha')
mask_only = layer.numpy(channel='mask')
shape_only = layer.numpy(channel='shape')
psd-tools show example.psd
# PSD全体をエクスポート
psd-tools export example.psd output.png

# 特定のレイヤーをエクスポート(インデックス指定)
psd-tools export example.psd[0] layer0.png
psd-tools debug example.psd
# レイヤーエフェクトの有無を確認
if layer.has_effects():
    # エフェクト情報にアクセス
    effects = layer.effects
# ベクターマスク情報
if layer.has_vector_mask():
    vector_mask = layer.vector_mask
    print(vector_mask)

# ライブシェイププロパティ
if layer.has_origination():
    for shape in layer.origination:
        # Rectangle, RoundedRectangle, Ellipse, Line など
        print(type(shape).__name__)
# クリッピングフラグ
print(layer.clipping)
layer.clipping = 1  # クリッピングマスクとして設定
from psd_tools.constants import Tag

# タグ付きブロックにアクセス
tagged_blocks = layer.tagged_blocks

# 特定のタグの情報を取得
if Tag.LAYER_ID in tagged_blocks:
    layer_id = tagged_blocks[Tag.LAYER_ID]
from pathlib import Path
from psd_tools import PSDImage

def export_all_layers(psd_path, output_dir):
    """PSDファイルの全レイヤーを個別の画像ファイルとして出力"""
    psd = PSDImage.open(psd_path)
    output_path = Path(output_dir)
    output_path.mkdir(exist_ok=True)
    
    for i, layer in enumerate(psd.descendants()):
        if layer.kind in ('pixel', 'shape'):
            try:
                image = layer.composite()
                # ファイル名をサニタイズ
                safe_name = "".join(c for c in layer.name if c.isalnum() or c in (' ', '-', '_'))
                image.save(output_path / f"{i:03d}_{safe_name}.png")
                print(f"Exported: {layer.name}")
            except Exception as e:
                print(f"Error exporting {layer.name}: {e}")

# 使用例
export_all_layers('example.psd', 'output_layers')
import json
from psd_tools import PSDImage

def extract_layer_info(psd_path):
    """レイヤー情報を辞書形式で抽出"""
    psd = PSDImage.open(psd_path)
    
    def layer_to_dict(layer):
        info = {
            'name': layer.name,
            'kind': layer.kind,
            'visible': layer.visible,
            'opacity': layer.opacity,
            'blend_mode': str(layer.blend_mode),
            'bbox': layer.bbox,
            'has_mask': layer.has_mask(),
        }
        
        if layer.is_group():
            info['children'] = [layer_to_dict(child) for child in layer]
        
        return info
    
    result = {
        'mode': psd.mode,
        'size': (psd.width, psd.height),
        'depth': psd.depth,
        'layers': [layer_to_dict(layer) for layer in psd]
    }
    
    return result

# 使用例
info = extract_layer_info('example.psd')
print(json.dumps(info, indent=2, ensure_ascii=False))
from pathlib import Path
from psd_tools import PSDImage

def batch_convert_psd_to_png(input_dir, output_dir):
    """ディレクトリ内の全PSDファイルをPNGに変換"""
    input_path = Path(input_dir)
    output_path = Path(output_dir)
    output_path.mkdir(exist_ok=True)
    
    for psd_file in input_path.glob('*.psd'):
        try:
            psd = PSDImage.open(psd_file)
            image = psd.composite()
            
            output_file = output_path / f"{psd_file.stem}.png"
            image.save(output_file)
            print(f"Converted: {psd_file.name} -> {output_file.name}")
        except Exception as e:
            print(f"Error converting {psd_file.name}: {e}")

# 使用例
batch_convert_psd_to_png('input_psds', 'output_pngs')
# 低レベルデータ構造にアクセス
psd._record  # 低レベルPSDレコード
pip install 'psd-tools[composite]'
# Ubuntu/Debian
apt-get install liblcms2-2

# macOS
brew install little-cms2

# Pillowを再インストール
pip uninstall Pillow
pip install Pillow

よくある質問:PythonでPSDをPNGに変換する

psd-toolsとは、何ですか?

PythonでPSDのレイヤー情報を読み取り、画像を出力するためのライブラリです。Photoshopを起動せずに処理できますが、Photoshopの描画結果をすべて再現するものではありません。記事では、見本画像の変換、レイヤーの検査、単純なピクセルレイヤーの処理に分けて使うとしています。

psd-toolsで、何ができて、何は確認が必要ですか?

PSD全体をPNGにする(PSDImage.open()とcomposite()。保存済みプレビューを使う場合と、レイヤーから合成する場合を区別します)、レイヤーを検査する(名前・種類・可視性・位置などを読み、グループ内も読む場合はdescendants()を使います)、ピクセルレイヤーを取り出す(layer.composite()。単独レイヤーの出力と、文書全体の見た目は異なります)、名前や可視性を変更してPSDを別名で保存する、ができます。文字内容の差し替え・複雑な表現の再現は、APIの対応範囲を確認してください。公式のUsageは、合成結果がPhotoshopと異なる場合があること、調整レイヤーやレイヤー効果の対応に制約があることを説明しており、納品用の画像はPhotoshopの書き出し結果と照合してください。

保存済みプレビューを使わずに合成するには、どうしますか?

ignore_preview=Trueを指定します。この指定は「Photoshopと同じ結果にする」という意味ではなく、保存済み画像を読む処理から、psd-toolsでレイヤーを合成する処理へ切り替える指定です。透明度や効果が変わる場合は、両方の画像を比較して原因を調べます。記事の自作PSDでは、保存済みプレビューから得た画像と、ignore_preview=Trueで得た画像に透明度の差がありました。

Photoshopで作ったPSDでは、ignore_preview=True にするとドロップシャドウが消え、指定なしのほうがPhotoshopの結果とほぼ一致しました。

visibleとis_visible()は、何が違いますか?

visibleはそのレイヤー自身の設定で、is_visible()は親グループの状態も反映します。記事の自作サンプルでは、青いレイヤー自身を表示にしても、親グループを非表示にすると、visibleは真、is_visible()は偽になりました。表示対象を抽出する場合、この違いを確認してください。

レイヤーを個別のPNGとして保存するには?

表示対象のピクセルレイヤーだけを出力する例で、連番を付け、ファイル名に使わない記号を置き換えます。シェイプ、文字、スマートオブジェクトを、この例だけで完全に変換するとはしていません。レイヤー単体の画像サイズと、PSDのキャンバス上の位置は分けて扱い、配置も再現する場合はbboxの座標を残してください。Photoshop自身でレイヤーを書き出す手順は、「レイヤーを個別ファイルに書き出す」記事を参照してください。

レイヤー名を変更して保存するときの注意点は?

保存先には未使用のファイル名を指定し、保存後にPSDを開き直して変更を確認します。レイヤー構造や属性の変更により、保存時の合成プレビューが作り直される場合があるため、元のPhotoshop表示を維持できるかは別途確認してください。低レベルのレコード操作、文字内容の編集、リンクされたスマートオブジェクトの置き換えは、記事の確認対象に含めていません。

エラーが出たときは、何を確認しますか? 記事の結果は実機で確認されていますか?

ModuleNotFoundErrorなら、実行するPythonとpipで入れた環境が同じか確認します。文書のメソッドが存在しない場合は、インストールした版と参照している文書の版を確認します。高度な合成の依存関係を要求される場合は、公式文書のcomposite追加依存関係と対象レイヤーを確認します。非表示グループの子が出力対象になる場合は、visibleとis_visible()の取り違えを疑います。透明度や色、効果が変わる場合は、プレビュー利用・再合成の違い、ICC、レイヤーの対応範囲を確認します。レイヤー画像の保存で止まる場合は、画像がNoneか、保存名が重複していないか、出力先の書き込み権限を確認します。なお、記事の実行結果は2026年10月3日にWindows上のPythonで、psd-tools 1.21.0、Pillow 12.3.0、NumPy 2.5.3を使い、自作の640×480px・8bitのサンプルで確認したもので、Photoshopのスクリーンショットではありません。旧記事の広いAPI例は比較資料として末尾に残してあり、現在の版で全例を検証したものではありません。最新は、公式Usage、プロジェクトのGitHub、PyPIで確認してください。

2026年10月17日には、Photoshop 2026(27.11.0)で作ったPSDでも掲載コードを動かし、書き出したPNGをPhotoshopで開いて見比べました。

結果は「Photoshopで作ったPSDで確かめた結果」の節にあります。

次に学ぶ・作業環境を選ぶ

学習を続けたい方や、作業環境を整えたい方は、目的に合うガイドをご覧ください。

Photoshopのおすすめ書籍

動作要件とPCの確認項目

周辺機器の優先度

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次