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

PhotoshopのCEPカスタムパネルを作る方法|最小構成・JSX連携・表示されない原因

PhotoshopのCEPカスタムパネルは、HTMLのボタンからExtendScript(JSX)を呼び出す仕組みです。既存のCEPパネルを保守する人向けに、manifest・画面・JSXをそろえる最小例と、表示されないときの確認順を説明します。新しく長期運用するパネルを開発する場合は、UXPプラグインの開発方法を確認してください。

今回の例はWindowsのCEP開発用サンプルです。Photoshopでの読み込み・グループ追加・新しい操作画面の撮影は未検証です。旧記事の画面とコードは後半に保存し、現在の実行結果とは区別しています。

目次

CEPとUXP、アプリ版とランタイム版を分ける

項目この例で指定するもの
Photoshopのホスト版manifestの[27.0,28.0)は27.xを対象として宣言する範囲。動作保証ではない。
CEPランタイムRequiredRuntimeのCSXS 12.0は要求する最低版。Photoshopの27という番号とは別。
画面側HTML・CSS・JavaScript。CSInterface.jsを通じて呼び出す。
Photoshop側ExtendScriptの.jsx。UXPの.psjsを同じファイルとして扱わない。

AdobeのCEP 12 CookbookではPhotoshop 25.12とCEP 12の組合せが掲載されています。利用中の版・OSがCEPを読み込めるかを別に確認してください。Apple SiliconのPhotoshopで旧CEPを使う条件はAdobeのRosettaに関する案内で確認できます。

2026年9月のAdobeの移行計画は、Photoshopの新規CEP受付を2027年3月に終了、2027年12月に初期状態で無効化、2029年12月に新しいアプリ版から除去する予定を示しています。ExtendScript自体はこの移行の対象外です。現在すでに除去されたという意味ではありません。

5つのファイルとCSInterface.jsを配置する

Windowsでは、%APPDATA%\Adobe\CEP\extensionsの下にcom.cgmethod.cep.sampleフォルダーを作成します。別の場所に保管した開発元をコピーし、既存パネルを上書きしない固有IDを使ってください。Bracketsは必須ではありません。AdobeはBracketsのサポートを2021年9月1日に終了しました。Adobeリポジトリの案内を確認し、UTF-8で保存できるエディターで編集します。

com.cgmethod.cep.sample/
  CSXS/manifest.xml
  index.html
  css/style.css
  js/CSInterface.js
  js/main.js
  jsx/hostscript.jsx

CSInterface.jsはAdobe CEP 12の公式ライブラリを保存し、jsフォルダーへ配置します。以下の5ファイルは同じサンプルの構成です。jQuery・外部CDN・個人の絶対パス・動的HTML読込は使いません。

CSXS/manifest.xml

<?xml version="1.0" encoding="UTF-8"?>
<ExtensionManifest Version="7.0" ExtensionBundleId="com.cgmethod.cep.sample" ExtensionBundleVersion="1.0.0" ExtensionBundleName="CGM CEP Sample">
  <ExtensionList><Extension Id="com.cgmethod.cep.sample.panel" Version="1.0.0"/></ExtensionList>
  <ExecutionEnvironment>
    <HostList><Host Name="PHXS" Version="[27.0,28.0)"/></HostList>
    <LocaleList><Locale Code="All"/></LocaleList>
    <RequiredRuntimeList><RequiredRuntime Name="CSXS" Version="12.0"/></RequiredRuntimeList>
  </ExecutionEnvironment>
  <DispatchInfoList><Extension Id="com.cgmethod.cep.sample.panel"><DispatchInfo>
    <Resources><MainPath>./index.html</MainPath><ScriptPath>./jsx/hostscript.jsx</ScriptPath></Resources>
    <Lifecycle><AutoVisible>true</AutoVisible></Lifecycle>
    <UI><Type>Panel</Type><Menu>CGM CEP Sample</Menu><Geometry><Size><Height>220</Height><Width>280</Width></Size></Geometry></UI>
  </DispatchInfo></Extension></DispatchInfoList>
</ExtensionManifest>

index.html

<!doctype html>
<html lang="ja"><head><meta charset="utf-8"><title>CGM CEP Sample</title>
<link rel="stylesheet" href="css/style.css"></head><body>
<button id="make-group" type="button">テストグループを追加</button>
<p id="result" role="status">未実行</p>
<script src="js/CSInterface.js"></script><script src="js/main.js"></script>
</body></html>

css/style.css

body { margin: 12px; background: #303030; color: #eee; font: 14px sans-serif; }
button { width: 100%; padding: 10px; }
#result { overflow-wrap: anywhere; }

js/main.js

(function () {
    "use strict";
    var button = document.getElementById("make-group");
    var result = document.getElementById("result");
    if (typeof CSInterface !== "function") {
        button.disabled = true;
        result.textContent = "CSInterface.jsとCEP内での読込を確認してください。";
        return;
    }
    var cs = new CSInterface();
    button.addEventListener("click", function () {
        button.disabled = true;
        result.textContent = "処理中";
        try {
            cs.evalScript("cgmMakeGroup()", function (response) {
                button.disabled = false;
                result.textContent = response || "応答がありません。文書とログを確認してください。";
            });
        } catch (error) {
            button.disabled = false;
            result.textContent = "呼出し失敗: " + error.message;
        }
    });
}());

jsx/hostscript.jsx

function cgmMakeGroup() {
    if (app.documents.length === 0) {
        return "ERROR: 文書のコピーを開いてください。";
    }
    try {
        var doc = app.activeDocument;
        var group = doc.layerSets.add();
        group.name = "CGM_panel_test";
        return "OK: " + doc.name + " / " + group.name;
    } catch (error) {
        return "ERROR: " + error.message;
    }
}

MainPathが画面の入口、ScriptPathがPhotoshop側の関数を読み込む指定です。main.jsからcgmMakeGroup()を呼び、JSXの戻り値を画面に表示します。ボタンを押すと開いた文書にグループを追加するため、原本ではなくコピーで確認してください。この例はファイルを保存しません。

未署名パネルの開発設定と復元

署名済みの配布物と、未署名の開発元は扱いが異なります。自作の未署名パネルを読む場合だけ、使用中のCEPに合うPlayerDebugModeを設定します。CEP 12 CookbookのWindows例はHKEY_CURRENT_USER\Software\Adobe\CSXS.12に文字列値PlayerDebugModeを作り、値を1にします。CSXS.7をすべてのPhotoshopに使う設定ではありません。

  • 変更前に対象キーとPlayerDebugModeの有無・種類・値を記録する。既存値を消さない。
  • 変更後にPhotoshopを再起動し、ウィンドウ→エクステンション(従来)など利用版のメニューからCGM CEP Sampleを探す。
  • 確認後は、以前からあった値なら元の値へ戻す。自分が追加した値ならその値だけを削除する。無関係なキーやほかの開発設定は消さない。

画面表示とJSX実行を順に確認する

段階期待する状態・記録
表示CGM CEP Sampleのボタンと「未実行」が見える。これだけでJSX成功とはしない。
文書なしボタンを押すとERROR: 文書のコピーを開いてください。新規文書を勝手に作らない。
文書のコピーあり元のグループ数を控え、1回押すとCGM_panel_testが1つ増える。OK:に表示された文書名を照合する。
復元元に戻すで追加グループを戻すか、保存せずコピーを閉じる。原本が変わっていないことを確認する。

以上は確認用の期待値です。このPCでのCEP実行結果ではありません。保存を伴う自作JSXを組み込む場合は、枚数・ファイル名・内容・保存先を開き直して照合し、パネルのOK表示だけで完了とは判断しないでください。

表示されない・ボタンが動かない場合の確認順

症状確認する箇所
メニューにないCEPに対応する起動方式、配置先、拡張IDの重複、PHXSとホスト版の範囲、要求ランタイム。範囲を広げても互換性は増えない。
未署名の警告使用中のCEPに合うPlayerDebugModeのキー・文字列型・値。無関係なCSXSキーへ1を追加し続けない。
パネルが空MainPathとindex.html、JavaScript/CSSの相対パス、ファイル名・文字コード。
CSInterfaceの読込エラー公式ライブラリの保存位置とscriptの順序。通常ブラウザーだけではPhotoshopのCEP APIは使えない。
EvalScript error.や関数未定義ScriptPath・JSXの関数名・保存後の再読込とAdobeのCEPログを確認。
途中の処理失敗対象文書・レイヤー・色モードなどJSXの条件を確認する。処理を自動で繰り返さず、文書の変化も確認する。

ログと開発者ツールの手順はAdobeのクライアント側デバッグ資料を参照してください。CSS・HTML・JSXを保存しただけで、開いているパネルが必ず再読込されるとは限りません。いったんパネルを読み直し、必要ならPhotoshopを再起動して変更を照合します。

タブ・折りたたみ・外部JSXへ拡張する

最小例の呼出しが通ったあとで、ボタンを追加し、HTMLのidと関数名を1組ずつ合わせます。タブはボタンと内容領域を同じ数にし、最初の領域だけを表示します。JSXを別ファイルに分ける場合は、拡張ルートからの相対配置と読込順を明示してください。利用者名を埋め込んだ絶対パスは移行時に壊れます。

旧記事のタブ・折りたたみ・CSSは後半の参照資料に保存しました。外部HTMLをiframeで取り込む旧include.jsは引用符が崩れているため、現行の最小例では使いません。必要なUIをindex.htmlへ配置し、1つずつ確認してください。

配布時の署名とZXP

配布用CEPは、開発時のデバッグ許可を前提にしない形でパッケージ化します。Adobeの署名・配布資料でZXPSignCmdと証明書を確認してください。署名後にファイルを変更すると同じ署名での検証対象ではなくなります。今回の例では署名・ZXP導入を実行していません。旧署名画面のtestは例示用で、自分の証明書とパスワードを公開コードへ保存しないでください。

旧記事の画面記録

旧カスタムパネルのタブとボタンの画面。現行版の動作結果ではありません。
旧カスタムパネルのタブとボタンの画面。現行版の動作結果ではありません。 旧記事の掲載画像です。撮影日・詳細版は未確認です。
旧BracketsでCC Extension BuilderをURLから導入した画面。現在の導入手順ではありません。
旧BracketsでCC Extension BuilderをURLから導入した画面。現在の導入手順ではありません。 旧記事の掲載画像です。撮影日・詳細版は未確認です。
旧CC Extension BuilderでHello Worldを生成した画面。
旧CC Extension BuilderでHello Worldを生成した画面。 旧記事の掲載画像です。撮影日・詳細版は未確認です。
旧manifest.xmlのPhotoshop 18.0〜18.9の指定。今回の27.x向け例とは異なります。
旧manifest.xmlのPhotoshop 18.0〜18.9の指定。今回の27.x向け例とは異なります。 旧記事の掲載画像です。撮影日・詳細版は未確認です。
旧Photoshopのウィンドウ→エクステンションにHello Worldが表示された画面。
旧Photoshopのウィンドウ→エクステンションにHello Worldが表示された画面。 旧記事の掲載画像です。撮影日・詳細版は未確認です。
Photoshop CC 2017の未署名拡張エラー。現行版の再現結果ではありません。
Photoshop CC 2017の未署名拡張エラー。現行版の再現結果ではありません。 旧記事の掲載画像です。撮影日・詳細版は未確認です。
旧CSXS.7のPlayerDebugMode設定。CEP 12の設定先を示す画像ではありません。
旧CSXS.7のPlayerDebugMode設定。CEP 12の設定先を示す画像ではありません。 旧記事の掲載画像です。撮影日・詳細版は未確認です。
旧Hello WorldパネルからExtendScriptのアラートを表示した記録。
旧Hello WorldパネルからExtendScriptのアラートを表示した記録。 旧記事の掲載画像です。撮影日・詳細版は未確認です。
旧ZXPSignCmdの署名コマンドと成功表示。例示用パス・パスワードで、今回の署名結果ではありません。
旧ZXPSignCmdの署名コマンドと成功表示。例示用パス・パスワードで、今回の署名結果ではありません。 旧記事の掲載画像です。撮影日・詳細版は未確認です。
旧TESTパネルのCSXS、icons、index.htmlの構成。
旧TESTパネルのCSXS、icons、index.htmlの構成。 旧記事の掲載画像です。撮影日・詳細版は未確認です。
旧TESTパネルでアイコンを表示した記録。
旧TESTパネルでアイコンを表示した記録。 旧記事の掲載画像です。撮影日・詳細版は未確認です。
旧カスタムパネルのタブとボタンの画面。現行版の動作結果ではありません。
旧カスタムパネルのタブとボタンの画面。現行版の動作結果ではありません。 旧記事の掲載画像です。撮影日・詳細版は未確認です。
旧グループ作成パネルとレイヤーパネル。今回の最小例の実行結果ではありません。
旧グループ作成パネルとレイヤーパネル。今回の最小例の実行結果ではありません。 旧記事の掲載画像です。撮影日・詳細版は未確認です。
旧コードの参照資料(実行しない)

以下は旧記事のコードを保存した参照資料です。新しい例と混ぜて実行しないでください。旧配布リポジトリも今回更新していません。

Bracketsによる雛形、CSXS.7、CC 2017の18.x指定、絶対パスのincludeと動的HTML読込を含みます。include.jsには引用符の不整合、HTMLにはタグ間の余分な空白があり、そのままの実行用ではありません。

旧コード 1

<Host Name="PHXS" Version="[15.0,15.9]" />
<Host Name="PHSP" Version="[15.0,15.9]" />

旧コード 2

<Host Name="PHSP" Version="[18.0,18.9]" />
<Host Name="PHXS" Version="[18.0,18.9]" />

旧コード 3

Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Adobe\CSXS.7]
"PlayerDebugMode"="1"

旧コード 4

ZXPSignCmd -selfSignedCert JP Tokyo My MIN test custompanel.test

旧コード 5

ZXPSignCmd -sign "C:\Users\ユーザー名\AppData\Roaming\Adobe\CEP\extensions\com.example.helloworld" custompanel.zxp custompanel.test test

旧コード 6

<?xml version="1.0" encoding="UTF-8"?>
<ExtensionManifest Version="5.0" ExtensionBundleId="com.example.test" ExtensionBundleVersion="1.0"
ExtensionBundleName="TEST" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <ExtensionList>
        <Extension Id="com.example.test" Version="1.0" />
    </ExtensionList>
    <ExecutionEnvironment>
        <HostList>
            <Host Name="PHSP" Version="[18.0,18.9]" />
            <Host Name="PHXS" Version="[18.0,18.9]" />
        </HostList>
        <LocaleList>
            <Locale Code="All" />
        </LocaleList>
        <RequiredRuntimeList>
            <RequiredRuntime Name="CSXS" Version="5.0" />
        </RequiredRuntimeList>
    </ExecutionEnvironment>
    <DispatchInfoList>
        <Extension Id="com.example.test">
            <DispatchInfo>
                <Resources>
                    <MainPath>./index.html</MainPath>
                </Resources>
                <Lifecycle>
                    <AutoVisible>true</AutoVisible>
                </Lifecycle>
                <UI>
                    <Type>Panel</Type>
                    <Menu>TEST</Menu>
                    <Geometry>
                        <Size>
                            <Height>300</Height>
                            <Width>300</Width>
                        </Size>
                    </Geometry>
                </UI>
            </DispatchInfo>
        </Extension>
    </DispatchInfoList>
</ExtensionManifest>

旧コード 7

<html>
<head></head>
<body>
    <img src="icons/suimin.png">
</body>
</html>

旧コード 8

#include "/c/Users/ユーザー名/AppData/Roaming/Adobe/CEP/extensions/custompanel/jsx/scripts/AAA.jsx"

旧コード 9

$("#AAA").click(function () {
    csInterface.evalScript('AAA()');
});

旧コード 10

$("#AAA").click(function () {
    csInterface.evalScript('AAA()');
});

旧コード 11

<!-- 折りたたみ -->
<div id="AAAAA" class="topcoat-button--large--cta hostFontSize" onclick="obj=document.getElementById('BBBBB').style; obj.display=(obj.display=='none')?'block':'none';">
便利ツール
</div>
<!--// 折りたたみ -->
<!-- 折りたたまれ -->
<div id="BBBBB" style="display:none;clear:both;">
<button id="AAA" class="topcoat-button--large hostFontSize">あいうえお</button>
</div>
<!--// 折りたたまれ -->

旧コード 12

<!-- 折りたたみ -->
<div id="AAAAA" class="topcoat-button--large--cta hostFontSize" onclick="obj=document.getElementById('BBBBB').style; obj.display=(obj.display=='block')?'none':'block';">
便利ツール
</div>
<!--// 折りたたみ -->
<!-- 折りたたまれ -->
<div id="BBBBB" style="display:block;clear:both;">
<button id="AAA" class="topcoat-button--large hostFontSize">あいうえお</button>
</div>

旧コード 13

<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>

旧コード 14

<script>
$(function() {
    $(".tab li").click(function() {
        var index = $(".tab li").index(this);
        $(".tabContent").removeClass('active');
        $(".tabContent").eq(index).addClass('active');
        $(".tab li").removeClass('active');
        $(this).addClass('active');
    });
});
</script>

旧コード 15

<ul class="tab">
    <li class="topcoat-button--large hostFontSize active">メニューA</li>
    <li class="topcoat-button--large hostFontSize">メニューB</li>
    <li class="topcoat-button--large hostFontSize">メニューC</li>
</ul>

旧コード 16

/* ボタンを中央寄せ */
div { text-align: center; }

/* ボタンサイズの統一 */
.topcoat-button--large--cta, .topcoat-button--large {
    width: 100%;
}

/* タブデザイン */
ul.tab {
    width: 100%;
    list-style: none;
    padding-left: 0;
}
ul.tab li {
    float: left;
    height: 30px;
    text-align: center;
    width: 33.3%;
    padding: 5px;
    background-color: #595b5b;
    color: #fff;
    cursor: pointer;
}
ul.tab li:hover {
    background-color: #539fe1;
}
ul.tab li.active {
    background-color: #368bdc;
}
div.tabContent {
    clear: both;
    width: 100%;
    display: none;
}
div.tabContent.active {
    display: block;
}

旧コード 17

function makeGroup(){
    var layer_face = app.activeDocument.layerSets.add();
    layer_face.name = '顔';
}

旧コード 18

(function () {
    'use strict';
    var csInterface = new CSInterface();
    function init() {
        $("#btn_test").click(function () {
            csInterface.evalScript('makeGroup()');
        });
    }
    init();
}());

旧コード 19

<button id="btn_test" class="topcoat-button--large hostFontSize">グループ作成</button>

旧コード 20

#include "/C/Users/Namae/AppData/Roaming/Adobe/CEP/extensions/custompanel/jsx/hostscript.jsx"

旧コード 21

<script src="C:/Users/Namae/AppData/Roaming/Adobe/CEP/extensions/custompanel/js/main.js"></script>

旧コード 22

function include(filename, afterfunc) {
    include.seq = (include.seq) ? include.seq + 1 : 1;
    var id = new Date().getTime() + "-" + include.seq;
    var inc = document.createElement("iframe");
    inc.id = "inc-" + id;
    inc.src = filename;
    inc.style.display = "none";
    document.write("<span id="" + id + ""></span>");
    var incfunc = function() {
        var s = (function() {
            var suffix = (n = filename.lastIndexOf(".")) >= 0 ? filename.substring(n) : "default";
            if (suffix == ".html") return inc.contentWindow.document.body.innerHTML;
            if (suffix == ".txt") return inc.contentWindow.document.body.firstChild.innerHTML;
            if (suffix == "default") return inc.contentWindow.document.body.innerHTML;
        })();
        var span = document.getElementById(id);
        var insertBeforeHTML = function(htmlStr, refNode) {
            if (document.createRange) {
                var range = document.createRange();
                range.setStartBefore(refNode);
                refNode.parentNode.insertBefore(range.createContextualFragment(htmlStr), refNode);
            } else {
                refNode.insertAdjacentHTML('BeforeBegin', htmlStr);
            }
        };
        insertBeforeHTML(s.split(">").join(">").split("<").join("<"), span);
        document.body.removeChild(inc);
        span.parentNode.removeChild(span);
        if (afterfunc) afterfunc();
    };
    if (window.attachEvent) {
        window.attachEvent('onload',
            function() {
                document.body.appendChild(inc);
                inc.onreadystatechange = function() {
                    if (this.readyState == "complete") incfunc();
                };
            });
    } else {
        document.body.appendChild(inc);
        inc.onload = incfunc;
    }
}

旧コード 23

< !doctype html >
    < html >
    < head >
    < meta charset = "utf-8" >
    < link rel = "stylesheet"
href = "css/topcoat-desktop-dark.min.css" / >
    < link id = "hostStyle"
rel = "stylesheet"
href = "css/styles.css" / >
    < script type = "text/javascript"
src = "https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js" > < /script> < script >
    $(function() {
        $(".tab li").click(function() {
            var num = $(".tab li").index(this);
            $(".tabContent").removeClass('active');
            $(".tabContent").eq(num).addClass('active');
            $(".tab li").removeClass('active');
            $(this).addClass('active')
        });
    }); < /script> < title > < /title> < /head> < body class = "hostElt" >
    < ul class = "tab" >
    < li class = "topcoat-button--large hostFontSize active" > メニューA < /li> < li class = "topcoat-button--large hostFontSize" > メニューB < /li> < li class = "topcoat-button--large hostFontSize" > メニューC < /li> < /ul> < script type = "text/javascript"
src = "js/include.js" > < /script> < script type = "text/javascript" >
    include("C:/Users/Namae/AppData/Roaming/Adobe/CEP/extensions/custompanel/contents.html"); < /script>

配布元と関連する実行方法

旧コードの配布元はMOTOKI-LLC/cg-method-codeです。今回の最小例とは別の参照資料で、リポジトリを更新していません。

JSXを参照から実行する方法、既存JSXLauncherの条件、UXPプラグインも確認できます。

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

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

Photoshopのおすすめ書籍

動作要件とPCの確認項目

周辺機器の優先度

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