Amazonプライム感謝祭開催中!セール会場へ

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

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

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

目次

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でレイヤーを合成する処理へ切り替える指定です。透明度や効果が変わる場合は、両方の画像を比較して原因を調べます。

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

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

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

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

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

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

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自身でレイヤーを書き出す手順は、レイヤーを個別ファイルに書き出す記事を参照してください。

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

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

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

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

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

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() は今回の環境では使用できず、文書側でレイヤーを作成してグループへ追加しました。コードを転用するときは、文書の版とインストールした版を照合してください。

文字、調整レイヤー、複雑なマスク・効果、16bit・32bit、Photoshopの描画結果との一致は未確認です。これらを含むファイルは、代表サンプルで差を確認してから一括処理へ進みます。

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

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

症状 調べる内容
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で得た画像に透明度の差がありました。

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で確認してください。

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

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

Photoshopのおすすめ書籍

動作要件とPCの確認項目

周辺機器の優先度

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