見出し画像

【Illustrator】スクリプトでパス上文字の扱いを高める

Illustratorのテキストには、3つのテキストの種類があります。

  • ポイント文字

  • エリア内文字

  • パス上文字

「ポイント文字←→エリア内文字」は変換機能が用意されていますが、パス上文字だけは独立しています。

パス上文字は古くからある機能ですが、なかなか扱いが面倒です。


テキストとパスをパス上文字に変換する

たとえば、テキストとパスがあり、それらを元にパス上文字にするのは面倒な作業です。

また、円にテキストを這わせようと、[パス上文字ツール]を選択してパス上でクリックすると、エリア内文字になってしまい……は、誰でも経験している困った挙動です。

そこで、テキストとパスをパス上文字に変換するスクリプトを作ってみました。

スクリプト

コピーしてテキストエディタにペーストして保存してお使いください。拡張子を.jsxに変更すればスクリプトファイルとして実行できます(有料記事になっていますが、ご自身で作業なされば無料です)。

/*
### スクリプト名:
ポイント文字とパスをパス上文字に変換

### 概要:
選択中の「ポイント文字」を、同時に選択しているパス(PathItem)上の文字に変換します。
- パスが一緒に選択されている場合:そのパスを複製して使用(複数テキストにも対応)
- パスが選択されていない場合:処理を実行せず終了します(パス必須)

License: MIT
See LICENSE file for details.

Original: Toshiyuki Takahashi
https://note.com/gautt/n/n92f6faeda048

---
*/

(function () {

    var SCRIPT_VERSION = "v1.0";

    // --- Localization (ja/en) ---
    function getCurrentLang() {
        return ($.locale && $.locale.indexOf('ja') === 0) ? 'ja' : 'en';
    }
    var lang = getCurrentLang();

    var LABELS = {
        noDocument: { ja: 'ドキュメントが開かれていません', en: 'No document is open.' },
        noTargetText: { ja: '対象のポイント文字が見つかりません', en: 'No target point text found.' },
        needPath: { ja: 'パスを一緒に選択してください', en: 'Please select a path together with the text.' },
        duplicateFailed: { ja: 'パスの複製に失敗しました', en: 'Failed to duplicate the path.' }
    };

    function L(key) {
        var o = LABELS[key];
        if (!o) return key;
        return o[lang] || o.en || key;
    }

    // Get items
    if (app.documents.length === 0) {
        alert(L('noDocument'));
        return false;
    }
    var doc = app.activeDocument;
    var sel = doc.selection;

    var targetItems = getTargetTextItems(sel);
    var selectedPaths = getSelectedPathItems(sel);

    // Validation
    if (targetItems.length === 0) {
        alert(L('noTargetText'));
        return false;
    }

    if (!selectedPaths || selectedPaths.length === 0) {
        alert(L('needPath'));
        return false;
    }

    main(targetItems, selectedPaths);

    // Main process
    function main(targetItems, selectedPaths) {

        // Track original selected paths for later removal (avoid duplicates).
        // NOTE:
        // - Only the *original selected paths* are removed at the end.
        // - The duplicated path used for textPath remains in the document.
        var usedPaths = [];

        for (var j = 0; j < targetItems.length; j++) {
            // Get original text frame item & current layer
            var originalText = targetItems[j];
            var currentLayer = originalText.layer;

            // Resolve base path + duplicate path for this text
            var basePath = resolveBasePathForText(selectedPaths, targetItems.length, j);
            if (!basePath) {
                // Should not happen because of Validation, but keep defensive
                alert(L('needPath'));
                continue;
            }

            // Track used paths for removal (avoid duplicates)
            pushUnique(usedPaths, basePath);

            var textPath = duplicatePathForText(basePath, currentLayer);
            if (!textPath) {
                alert(L('duplicateFailed'));
                continue;
            }

            // Create Text on a path
            var textOnAPath = currentLayer.textFrames.pathText(textPath);

            // Duplicate textrange from original text frame item
            for (var i = 0; i < originalText.textRanges.length; i++) {
                originalText.textRanges[i].duplicate(textOnAPath);
            }

            // Remove original text frame item
            safeRemove(originalText);

            // Select text on a path
            textOnAPath.selected = true;
        }

        // Remove only the original selected paths (not the duplicated textPath).
        // The duplicated paths created for path text must remain.
        for (var p = 0; p < usedPaths.length; p++) {
            safeRemove(usedPaths[p]);
        }
    }

    function safeRemove(item) {
        try {
            if (!item) return;
            item.remove();
        } catch (_) { }
    }

    // Push only if not already in the array (by reference)
    function pushUnique(arr, item) {
        for (var i = 0; i < arr.length; i++) {
            if (arr[i] === item) return;
        }
        arr.push(item);
    }

    // Decide which selected path to use for each text.
    // Rule 1: If the number of selected paths equals the number of texts,
    //         use the path at the same index (1-to-1 correspondence).
    // Rule 2: Otherwise, use the first selected path for all texts.
    function resolveBasePathForText(selectedPaths, textCount, index) {
        if (!selectedPaths || selectedPaths.length === 0) return null;
        if (selectedPaths.length === textCount) return selectedPaths[index];
        return selectedPaths[0];
    }

    // Resolve actual PathItem to duplicate (CompoundPathItem -> first pathItem)
    function resolveSourcePath(basePath) {
        if (!basePath) return null;

        if (basePath.typename === 'CompoundPathItem') {
            if (basePath.pathItems && basePath.pathItems.length > 0) {
                return basePath.pathItems[0];
            }
            return null;
        }

        return basePath;
    }

    // Duplicate a path for the text and place it on the same layer
    function duplicatePathForText(basePath, currentLayer) {
        var srcPath = resolveSourcePath(basePath);
        if (!srcPath) return null;

        try {
            var dup = srcPath.duplicate();
            tryMoveToLayer(dup, currentLayer);
            return dup;
        } catch (eDup) {
            return null;
        }
    }

    function tryMoveToLayer(item, layer) {
        try {
            if (!item || !layer) return;
            item.move(layer, ElementPlacement.PLACEATBEGINNING);
        } catch (_) { }
    }

    // Collect items recursively from GroupItem.pageItems using a predicate.
    // Why only GroupItem recursion?
    // - In Illustrator's object model, GroupItem is the general container that nests pageItems.
    // - PathItem and CompoundPathItem are collected as-is (CompoundPathItem is handled later when duplicating).
    // - Keeping recursion limited to GroupItem avoids unintended traversal into structures that behave differently
    //   (e.g. clipping/compound internals) and keeps selection-based behavior predictable.
    //
    // - items: collection/array of pageItems
    // - acceptFn: function(item) -> boolean
    function collectRecursive(items, acceptFn) {
        var out = [];
        if (!items) return out;

        for (var i = 0; i < items.length; i++) {
            var it = items[i];
            if (!it) continue;

            if (acceptFn && acceptFn(it)) {
                out.push(it);
            } else if (it.typename === 'GroupItem') {
                out = out.concat(collectRecursive(it.pageItems, acceptFn));
            }
        }
        return out;
    }

    // Get target point-text items (recursive)
    function getTargetTextItems(items) {
        return collectRecursive(items, function (it) {
            return (it.typename === 'TextFrame' && it.kind === TextType.POINTTEXT);
        });
    }

    // Get selected path items (PathItem or CompoundPathItem). If GroupItem contains paths, collect them too.
    function getSelectedPathItems(items) {
        return collectRecursive(items, function (it) {
            return (it.typename === 'PathItem' || it.typename === 'CompoundPathItem');
        });
    }

}());

ダウンロード

あふれ処理とフィット

別記事に書きました。

パス上文字の解除

さらに面倒なのがパス上文字の解除。絶望的に面倒です。
次をサポートするスクリプトを作成してみました。

  • テキストの書式:保持、削除

  • パス:1pt黒、透明、削除

スクリプト

コピーしてテキストエディタにペーストして保存してお使いください。拡張子を.jsxに変更すればスクリプトファイルとして実行できます(有料記事になっていますが、ご自身で作業なされば無料です)。

#target illustrator
app.preferences.setBooleanPreference('ShowExternalJSXWarning', false);

/*
### スクリプト名:
DetachPathText.jsx

### 更新日:
20260303

### 概要:
選択した「パス上文字(Path Text)」を、同じ文字内容・段落属性・文字属性をできるだけ維持したまま「ポイント文字」に変換します。
変換時に、「テキストの書式」およびパス設定を選択するダイアログが表示されます。
元のテキストパス形状は複製され、ダイアログの設定に応じて線属性(1pt黒/線なし)が適用されます。

### 使い方:
1) パス上文字を選択
2) スクリプトを実行
3) ダイアログでテキストの書式およびパスのオプションを選択

### 注意:
- 文字ごとの属性(フォント/サイズ/色/トラッキング等)は、可能な範囲で復元します。
- 復元時に設定できない属性は try/catch でスキップします。
*/

/* バージョン / Version */
var SCRIPT_VERSION = "v1.0.6";

/* 言語判定 / Language detection */
function getCurrentLang() {
    return ($.locale.indexOf("ja") === 0) ? "ja" : "en";
}
var lang = getCurrentLang();

/* 日英ラベル定義 / Japanese-English label definitions */
var LABELS = {
    dialogTitle: { ja: "パス上文字の解除", en: "Detach Path Text" },
    pnlKeep: { ja: "テキストの書式", en: "Text Formatting" },
    rbFull: { ja: "完全に保持", en: "Preserve Completely" },
    rbFast: { ja: "高速に保持", en: "Preserve Quickly" },
    rbNone: { ja: "削除", en: "Remove" },
    pnlPath: { ja: "パス", en: "Path" },
    rbPathBlack: { ja: "1pt黒に設定", en: "Set 1pt Black Stroke" },
    rbPathNone: { ja: "線なし", en: "No Stroke" },
    rbPathDelete: { ja: "削除", en: "Delete" },
    btnCancel: { ja: "閉じる", en: "Close" },
    btnOk: { ja: "OK", en: "OK" },
    alertNoDoc: { ja: "ドキュメントが開かれていません。", en: "No document is open." },
    alertNoSel: { ja: "パス上文字を選択してください。", en: "Please select path text." },
    alertNoPath: { ja: "選択範囲にパス上文字が含まれていません。", en: "No path text found in selection." }
};

/* ラベル取得ヘルパー / Label lookup helper */
function L(key) {
    return LABELS[key][lang];
}

// ==========================================
// UI: ダイアログ / Dialog
// ==========================================
function showOptionsDialog() {
    var dlg = new Window("dialog", L("dialogTitle") + " " + SCRIPT_VERSION);
    dlg.orientation = "column";
    dlg.alignChildren = ["fill", "top"];
    dlg.margins = 18;

    /* テキストパネル / Text panel */
    var pnlKeep = dlg.add("panel", undefined, L("pnlKeep"));
    pnlKeep.orientation = "column";
    pnlKeep.alignChildren = ["left", "top"];
    pnlKeep.margins = [15, 20, 15, 12];

    var rbFull = pnlKeep.add("radiobutton", undefined, L("rbFull"));
    var rbFast = pnlKeep.add("radiobutton", undefined, L("rbFast"));
    var rbNone = pnlKeep.add("radiobutton", undefined, L("rbNone"));
    rbFull.value = true;

    /* パスパネル / Path panel */
    var pnlPath = dlg.add("panel", undefined, L("pnlPath"));
    pnlPath.orientation = "column";
    pnlPath.alignChildren = ["left", "top"];
    pnlPath.margins = [15, 20, 15, 12];

    var rbPathBlack = pnlPath.add("radiobutton", undefined, L("rbPathBlack"));
    var rbPathNone = pnlPath.add("radiobutton", undefined, L("rbPathNone"));
    var rbPathDelete = pnlPath.add("radiobutton", undefined, L("rbPathDelete"));
    rbPathBlack.value = true; // デフォルト / Default

    /* ボタングループ / Button group */
    var grpBtns = dlg.add("group");
    grpBtns.orientation = "row";
    grpBtns.alignChildren = ["right", "center"];
    grpBtns.alignment = ["fill", "top"];

    var btnCancel = grpBtns.add("button", undefined, L("btnCancel"));
    var btnOk = grpBtns.add("button", undefined, L("btnOk"), { name: "ok" });

    var result = null;
    btnOk.onClick = function () {
        result = {
            mode: rbNone.value ? "none" : (rbFull.value ? "full" : "fast"),
            pathBlack1pt: rbPathBlack.value,
            pathNoStroke: rbPathNone.value,
            pathDelete: rbPathDelete.value
        };
        dlg.close(1);
    };
    btnCancel.onClick = function () {
        dlg.close(0);
    };

    var r = dlg.show();
    return (r === 1) ? result : null;
}

function main() {
    if (app.documents.length === 0) {
        alert(L("alertNoDoc"));
        return;
    }

    var opt = showOptionsDialog();
    if (!opt) return; // キャンセル / Cancelled

    var doc = app.activeDocument;
    var sel = doc.selection;

    if (sel.length === 0) {
        alert(L("alertNoSel"));
        return;
    }

    var pathTexts = [];

    /* 選択からパス上文字を抽出 / Extract path text from selection */
    for (var i = 0; i < sel.length; i++) {
        if (sel[i].typename === "TextFrame" && sel[i].kind === TextType.PATHTEXT) {
            pathTexts.push(sel[i]);
        }
    }

    if (pathTexts.length === 0) {
        alert(L("alertNoPath"));
        return;
    }

    /* パス上文字の変換処理 / Convert path text items */
    for (var j = pathTexts.length - 1; j >= 0; j--) {
        var pathText = pathTexts[j];
        var originalPath = pathText.textPath;

        // ==========================================
        // 1. テキスト属性のスナップショットを取得 / Snapshot text attributes
        // ==========================================

        // full: 文字ごとの属性まで保持 / full: preserve per-character attributes
        // fast: textRange.duplicate() で高速に保持 / fast: duplicate textRange quickly
        // none: 書式を削除(デフォルト書式) / none: remove formatting (use defaults)
        var charAttrs = null;
        var firstCharAttr = null;
        if (opt.mode === "full") {
            charAttrs = [];
            for (var c = 0; c < pathText.characters.length; c++) {
                var ch = pathText.characters[c];
                var ca = ch.characterAttributes;
                charAttrs.push({
                    font: ca.textFont,
                    size: ca.size,
                    fillColor: ca.fillColor,
                    strokeColor: ca.strokeColor,
                    strokeWeight: ca.strokeWeight,
                    tracking: ca.tracking,
                    baselineShift: ca.baselineShift,
                    horizontalScale: ca.horizontalScale,
                    verticalScale: ca.verticalScale,
                    autoLeading: ca.autoLeading,
                    leading: ca.leading
                });
            }
        } else if (opt.mode === "fast") {
            // fast(代表値スナップショット:将来の拡張用 / representative snapshot: for future use)
            if (pathText.characters.length > 0) {
                var ca0 = pathText.characters[0].characterAttributes;
                firstCharAttr = {
                    font: ca0.textFont,
                    size: ca0.size,
                    fillColor: ca0.fillColor,
                    strokeColor: ca0.strokeColor,
                    strokeWeight: ca0.strokeWeight,
                    tracking: ca0.tracking,
                    baselineShift: ca0.baselineShift,
                    horizontalScale: ca0.horizontalScale,
                    verticalScale: ca0.verticalScale,
                    autoLeading: ca0.autoLeading,
                    leading: ca0.leading
                };
            }
        }

        /* 段落属性(先頭段落から取得)/ Paragraph attributes (from first paragraph) */
        var justification = null;
        if (pathText.paragraphs.length > 0) {
            var paraAttr = pathText.paragraphs[0].paragraphAttributes;
            justification = paraAttr.justification;
        }

        var textContents = pathText.contents; // 文字列を保存 / Save string contents

        // ==========================================
        // 2. パスを複製 / Duplicate path
        // ==========================================
        if (!opt.pathDelete) {
            var newPath = doc.pathItems.add();

            for (var k = 0; k < originalPath.pathPoints.length; k++) {
                var origPt = originalPath.pathPoints[k];
                var newPt = newPath.pathPoints.add();
                newPt.anchor = origPt.anchor;
                newPt.leftDirection = origPt.leftDirection;
                newPt.rightDirection = origPt.rightDirection;
                newPt.pointType = origPt.pointType;
            }
            newPath.closed = originalPath.closed;
            newPath.filled = false;

            if (opt.pathNoStroke) {
                newPath.stroked = false;
            } else {
                newPath.stroked = true;

                if (opt.pathBlack1pt) {
                    var blackColor = new CMYKColor();
                    blackColor.cyan = 0;
                    blackColor.magenta = 0;
                    blackColor.yellow = 0;
                    blackColor.black = 100;

                    newPath.strokeColor = blackColor;
                    newPath.strokeWidth = 1;
                }
            }
        }

        // ==========================================
        // 3. 新しいポイントテキストを作成し属性を復元 / Create new point text and restore attributes
        // ==========================================
        var newText = doc.textFrames.add();

        /* 位置をパス開始点に合わせる / Align position to path start point */
        var anchorPoint = originalPath.pathPoints[0].anchor;
        newText.position = [anchorPoint[0], anchorPoint[1]];

        if (opt.mode === "fast") {
            // fast: textRange を丸ごと複製(高速)/ fast: duplicate entire textRange
            // ※内容・書式(段落/文字)をまとめて複製する / ※duplicates content and formatting together
            pathText.textRange.duplicate(newText);

            /* 文字色を確実に引き継ぐ / Ensure character colors are carried over */
            for (var rr = 0; rr < pathText.textRanges.length; rr++) {
                try {
                    var srcAttr = pathText.textRanges[rr].characterAttributes;
                    var dstAttr = newText.textRanges[rr].characterAttributes;
                    dstAttr.fillColor = srcAttr.fillColor;
                    dstAttr.strokeColor = srcAttr.strokeColor;
                } catch (eRR) { }
            }
        } else if (opt.mode === "none") {
            // none: 内容だけ流し込み / none: insert content only, use default formatting
            newText.contents = textContents;
        } else {
            // full: 内容を流し込み、文字ごとの属性を復元 / full: insert content and restore per-character attributes
            newText.contents = textContents;

            /* 段落属性の復元 / Restore paragraph attributes */
            if (justification !== null && newText.paragraphs.length > 0) {
                newText.paragraphs[0].paragraphAttributes.justification = justification;
            }

            // ★重要:新規テキストのデフォルト線を先に消す / ★Important: clear default stroke on new text first
            // 後続の文字ごとの復元で必要な場合のみ線が復元される / stroke is only restored if the original had one
            try {
                var nc = new NoColor();
                newText.textRange.characterAttributes.strokeColor = nc;
                newText.textRange.characterAttributes.strokeWeight = 0;
            } catch (eClr) { }
        }

        /* 文字属性の復元(full モードのみ)/ Restore character attributes (full mode only) */
        if (opt.mode === "full" && charAttrs) {
            for (var c = 0; c < newText.characters.length; c++) {
                var targetCa = newText.characters[c].characterAttributes;
                var srcCa = charAttrs[c];
                if (!srcCa) break;

                try { targetCa.textFont = srcCa.font; } catch (e) { }
                try { targetCa.size = srcCa.size; } catch (e) { }
                try { targetCa.fillColor = srcCa.fillColor; } catch (e) { }
                /* stroke: 元が「なし」の場合は strokeWeight を 0 に / stroke: set weight to 0 if original had no stroke */
                try {
                    var sc = srcCa.strokeColor;
                    targetCa.strokeColor = sc;
                    if (sc && sc.typename === "NoColor") {
                        targetCa.strokeWeight = 0;
                    } else {
                        targetCa.strokeWeight = srcCa.strokeWeight;
                    }
                } catch (e) { }
                try { targetCa.tracking = srcCa.tracking; } catch (e) { }
                try { targetCa.baselineShift = srcCa.baselineShift; } catch (e) { }
                try { targetCa.horizontalScale = srcCa.horizontalScale; } catch (e) { }
                try { targetCa.verticalScale = srcCa.verticalScale; } catch (e) { }
                try { targetCa.autoLeading = srcCa.autoLeading; } catch (e) { }

                /* autoLeading が false の場合のみ leading を設定 / Set leading only when autoLeading is false */
                if (!srcCa.autoLeading) {
                    try { targetCa.leading = srcCa.leading; } catch (e) { }
                }
            }
        }

        // ==========================================
        // 4. 元のパス上文字を削除 / Remove original path text
        // ==========================================
        pathText.remove();
    }
}

main();

スペシャル版

ポイント文字/パス上文字を、用途に応じて「作成」「分離」「調整」できるツールです。

  • パス上文字にする:選択したパス(または既存のパス上文字のパス)に沿ってテキストを配置

  • アーチ状のパスを生成:テキストからアーチのパスを自動生成してパス上文字を作成

  • 正円を生成:テキスト幅から正円パスを自動生成してパス上文字を作成

  • テキストを分離:パス上文字を「テキスト」と「パス」に分離(書式保持/保持しない、パス削除オプションあり)

  • 行揃え:左/中央/右/両端揃え

  • 効果:虹/歪み/3Dリボン/階段/引力

  • 位置:開始位置/終了位置(必要なときだけチェックONで適用)

  • テキスト調整:ベースライン/トラッキング/文字サイズ(現在値に対して増減)

  • フィット:パス上文字が端まで収まるように文字サイズを自動調整(開いたパスのみ)

  • プレビュー:ダイアログ操作中に結果を確認(OFFで元に戻せます)

  • テキスト編集:内容をまとめて置換(複数選択にも対応)

アップデート版

v1.3.3

  • 「アーチ状」にしたときの「まるみ」の度合いを調整できるようにした

  • デフォルト値の変更

ダウンロード

ここから先は

34字 / 2ファイル

¥ 300

Amazon Payで支払うと最大2%還元のチャンス! 9/30まで

定期マガジンを購読されるとサンプルファイルをダウンロードいただけます。 https://note.com/dtp_tranist/m/mebd7eab21ea5