Skip to content

Instantly share code, notes, and snippets.

@Yamonov
Last active December 6, 2025 08:38
Show Gist options
  • Select an option

  • Save Yamonov/3a90a9a50389c68095c5f14577ad09d5 to your computer and use it in GitHub Desktop.

Select an option

Save Yamonov/3a90a9a50389c68095c5f14577ad09d5 to your computer and use it in GitHub Desktop.
IllustratorのCMYK値を、CMYのうち2色+Kに置き換えるスクリプト

CMYK値をかっこよくするよ

選択したオブジェクトの塗りと線を、 CMYKのうちCMYのどれか2色+Kに置き換えます。

100%GPTコードです。好きに改造してください。 右上のDownload ZIPでDLして、jsxを実行。

何が嬉しいの?

RGBでもCMYKでも、原色が等量混ざるとグレーになります。(CMYの場合はインキの欠陥により、等量では赤みに寄ります。また、100%等量だと緑味を帯びます)

CMYKはK-C-M-Yと1色ずつ違うユニットで刷り重ねていったり、前に刷ったまだ乾いていないインキの上に刷ったりするため、 少しでも誤差が出ると、特にグレーは色相が大きく変わってしまいます。 SS_Safari_25_000707

CMYKには、3原色に加えて無彩色の黒、Kがあります。 実は、ほとんどのカラーはCMYのうちの2色または1色と、明度を変えるKで表現できます。

原色をすべて使わずに2色以内に抑えて、無彩色のKで濁すことで、印刷でブレる要素を減らすことができます。

SS_Safari_25_000709

特にStockなどにあるRGBなaiをCMYK変換したものなどで、色を濁らせない効果があります。

さらに、CMYのうち1〜2色+Kに制限するため、総インキ量が300%を超えることがありません。

image

分版プレビューパネルを出しておいて、ブラックをオフにしてから実行すると結果の違いがわかりやすいでしょう。また、こうしておけばIllustratorの状態が悪くて処理を取りこぼしたものも見つけやすくなります。

SS_ChatGPT_25_000711@2x

どうやってるの?

最初にCMYのうち1〜2色とKの組み合わせをapp.convertSampleColorでLabカラーにし、Labの三次元マップを作成(10ステップで1秒以下) ※プログレスバーのために、最初に荒くオブジェクト数を拾っています。複雑なオブジェクトでは時間がかかります。

取得したCMYK値をLabにし、マップ内にターゲットとして配置、球状に周辺探索してキーとなるLab値を複数取得 周辺のLabキーからCMYK値を取得し、ターゲットとのΔE距離に応じてCMYK値を調整、Lab変換して比較、を繰り返します。 Lab変換は、ドキュメントに埋め込まれたCMYKプロファイルか、無ければカラー設定のプロファイルを使います。

他には何をしているの?

小数点以下はround。

09/25版は、ほぼ完全に元の色を再現しますが、CMYK全てに3%以下は0に、97%以上は100に丸める処理を最後に入れています。このためこの付近のカラーは少し変わります。

GCR100%プロファイル変換でもできるけどこのスクリプト使うメリットは?

PhotoshopカスタムCMYKで、インキ、ドットゲインを適切に選択してGCR・墨版生成最大のプロファイルを作成してからIllustratorカラー設定に設定し、CMYK変換すれば確かに全てのオブジェクトのカラーが目的のものになりますが、カスタムCMYKプロファイルを当てていることを忘れないでください。この処理のあと、適切なCMYKプロファイルを指定しなおし、数値とカラーの結びつきを修正する必要があります。

このスクリプトはドキュメントプロファイルはそのままに、(多少の未対応オブジェクトはあるものの)色を変えずに数値を整理するものです。

目的に応じて使い分けてください。

注意

  • ❗❗かなり時間がかかります
    • Cacheを実装していますが、ほぼキャッシュヒットする場合でも、1,000オブジェクトに対し30秒程度かかります。
    • 分割したグラデーションのようなキャッシュの効かないものではさらに時間がかかります。
    • 事前にドキュメント情報でパスオブジェクト数を確認し、数千オブジェクトある場合はコーヒータイムにしてください。
    • 中止はできません。Illustratorを強制終了してください。
  • 元のオブジェクトのカラーを変えます。
  • もともと整理された値でも対象にします。
  • CMYK比率が変わるため、描画モード等の結果が変わることがあります。
  • 繰り返し(範囲限定しての)総当たり計算をしますので、オブジェクト数が多いと結構時間がかかります。また、メモリも消費します。
  • カラー分配は他の値も取り得ます。あくまでひとつの例と考えてください。
  • グラデーションメッシュは未対応です(グラデは対応)。
  • spotとgrayは無視します。
  • ブレンドは未対応です。分割してください。
  • 複合パスが残ることがありますが、一度解除&複合パス化すると処理されることがあります。
  • 不透明度のある塗りは、元の数値に対して処理するため大きく色が変わります。
  • Illustratorの状態によっては、取りこぼしが起きることがあります。Illustratorを再起動してクリーンにしてください

注意!!

元の色を印刷ブレに強くする配合にするスクリプトであり、色をきれいにするものではありません!

また、墨版が増えるために、印刷技量によってはかえってくすむこともあります。 自分で適宜K版の入りを調整してください。

2025/11/19:K削減オプションとUIを追加、不要なコードを削除(ありがとう @Creold)

2025/10/09:グローバルスウォッチに対応(特色は無視)

2025/09/27:コードのクリンアップのみ。

2025/09/26:黒補正を追加。RGB000に近い黒はKのみにします。

2025/09/25:全面的に書き換え。目標CMYK値からLabマップを作成し、ターゲットのCMYK値のLab変換値をマップ中に置き、最も近いLabキーからCMYK値をΔEの距離に応じて調整する方式に変更。ΔEは76です。速度はオブジェクトカラーのバリエーションで変わりますが、一般的なイラストなら以前より早くなっています。また色の精度が非常によくなっています。ただし、Illustratorの状態によってオブジェクトを取りこぼすことがあります。

2025/09/12:比較用のΔE基準を2000に変更。グレーの精度が少し上がっています。速度は少し落ちています。

2025/09/11:かなり高速化。高速化するにあたって精度を少し犠牲にしています。冒頭の変数を適当にいじってバランスをとってください。

#target illustrator
var YamoScriptVersion = "Ver 1.3.1 (2025-11-19)";
// CMYK値をCMYのうち1〜2版+Kに変換するスクリプト
// CMYKオブジェクトを選択して実行する。彩度調整オプションオフでは、ほぼ色を変えない
// 配布gist
// https://gist.github.com/Yamonov/3a90a9a50389c68095c5f14577ad09d5
// 2025-11-19:不必要な機能を無効化(ありがとう @Creold)
// K削減処理とダイアログを追加、グローバルスウォッチに対応(特色は維持)
// グラデーションメッシュ、ブレンドオブジェクトは非対応
// 複雑なパスのクリッピング内オブジェクトは取りこぼすことがあるため、分版プレビューでのチェック推奨
//-----------------------------------------------------
// 設定値: マップ生成の分解能(不等間隔ステップ/最大20要素)
var STEPS = [0, 10, 20, 30, 40, 50, 60, 70, 80, 85, 90, 95, 100];
// Lab格子の量子化幅(固定)
var QL = 1; // L*
var QA = 1; // a*(ab量子化・固定)
var QB = 1; // b*(ab量子化・固定)
// 近傍探索のΔE半径パラメータ(rCap/rHardCap は上限として扱う)
var R_START = 15; // 初期R(平均拡張回数をさらに下げる)
var R_STEP = 2; // Rの増分
var R_PAD_MAX = 5.0; // 保険拡張の最大追加幅
// 閾値(%): これ以下は0, これ以上は100 に丸め
var ZERO_THR = 3; // ≤ ZERO_THR → 0
// K削減オプション(後処理)
var K_TAPER_ENABLED = false; // ScriptUIで「彩度を上げる」がオンのときに有効化
var K_REDUCE_START = 5; // Kがこの値以下なら強制的に0に(ScriptUIで変更)
var K_REDUCE_END = 20; // Kがこの値以上なら変更しない(ScriptUIで変更)
// K-only を使うシャドウ判定のしきい値
var AB_TOL = 4; // |ab| <= AB_TOL ならニュートラル扱い
var L_MARGIN = 2; // 黒点Lからの許容マージン
var _L_BLACK_CACHE = null;
function getLBlack() {
if (_L_BLACK_CACHE === null) {
// RGB(0,0,0) → CMYK → Lab で黒点を取得(プロファイルに依存)
var cmyk = app.convertSampleColor(
ImageColorSpace.RGB, [0, 0, 0],
ImageColorSpace.CMYK, ColorConvertPurpose.defaultpurpose
);
var lb = cmykToLab(cmyk[0], cmyk[1], cmyk[2], cmyk[3]);
_L_BLACK_CACHE = lb.L;
}
return _L_BLACK_CACHE;
}
// 近傍微調整(Kも含めて同等に扱う)
// 局所探索の刻み(%):ステップリストの最小差分に基づくベース決定
function minStepDelta() {
var md = 100;
var last = null;
for (var i = 0; i < STEPS.length; i++) {
var v = STEPS[i];
if (last !== null) {
var d = v - last;
if (d > 0 && d < md) md = d;
}
last = v;
}
return (md === 100 ? 10 : md);
}
var REFINE_STEP = (minStepDelta() >= 10) ? 2 : 1; // 最小差分が粗いなら2%、細かければ1%
var MAX_REFINE_ITERS = 14; // 反復上限
// グローバル参照用のLabインデックス(apply系から参照)
var gLabIndex = null;
// 進行状況バー(ScriptUI): バーのみ、幅400px、分母は動的に+10%
var gProgress = null;
function createProgressBar(initialMax) {
var win = new Window('palette', 'CMYK値を整理中... ' + YamoScriptVersion, undefined, {
closeButton: false
});
var bar = win.add('progressbar', undefined, 0, Math.max(1, initialMax | 0));
bar.preferredSize = [400, 20];
win.layout.layout(true);
win.center();
win.show();
var state = {
win: win,
bar: bar,
value: 0,
max: Math.max(1, initialMax | 0)
};
return {
step: function (n) {
if (!n) n = 1;
state.value += n;
if (state.value > state.max) { // 分母を10%増やす
state.max = Math.ceil(state.max * 1.10);
state.bar.maxvalue = state.max;
}
state.bar.value = Math.min(state.value, state.max);
try {
state.win.update();
} catch (e) { }
},
close: function () {
try {
state.win.close();
} catch (e) { }
}
};
}
// 「変換オプション」ダイアログ
function showConvertOptionsDialog(pathCount) {
var dlg = new Window('dialog', '変換オプション');
dlg.orientation = 'column';
dlg.alignChildren = 'fill';
// バージョン表示(タイトルから移動)
var verGroup = dlg.add('group');
verGroup.orientation = 'row';
verGroup.alignment = 'left';
verGroup.add('statictext', undefined, YamoScriptVersion);
// 対象パス数表示
var infoGroup = dlg.add('group');
infoGroup.orientation = 'row';
infoGroup.add('statictext', undefined, '処理対象数: ' + pathCount);
// 「彩度を上げる」関連をまとめたパネル
var kPanel = dlg.add('panel');
kPanel.alignChildren = 'left';
kPanel.orientation = 'column';
// 「彩度を上げる」チェックとK削減開始/終了オプションを1行にまとめる
var optGroup = kPanel.add('group');
optGroup.orientation = 'row';
var chkSaturation = optGroup.add('checkbox', undefined, 'K濁り軽減');
chkSaturation.value = false; // 初期値はOFF
// K削減開始/終了オプション(1行にまとめる)
var labelK = optGroup.add('statictext', undefined, ' K削減開始');
var ddK = optGroup.add('dropdownlist', undefined, ['3', '5', '10', '15']);
ddK.selection = 1; // デフォルトは 5%
var labelRange = optGroup.add('statictext', [0, 0, 18, 18], '〜');
var labelKEnd = optGroup.add('statictext', undefined, '');
var ddKEnd = optGroup.add('dropdownlist', undefined, ['20', '30', '40', '50', '60']);
ddKEnd.selection = 0; // デフォルトは 20%
var labelPctEnd = optGroup.add('statictext', [0, 0, 14, 18], '%');
// updateEnabled 互換のためのエイリアス
var labelPct = labelPctEnd;
// 説明テキスト
var descGroup = kPanel.add('group');
descGroup.orientation = 'row';
descGroup.alignChildren = 'left';
var descText = descGroup.add(
'statictext',
[0, 0, 380, 80],
'K削除開始までのKをゼロにし、その分をCMYを増やして調整します。削除終了%までは緩やかに削減します。Kの太りからくる濁りを軽減します。', {
multiline: true
}
);
function updateEnabled() {
var en = chkSaturation.value;
labelK.enabled = en;
ddK.enabled = en;
labelPct.enabled = en;
labelKEnd.enabled = en;
ddKEnd.enabled = en;
labelPctEnd.enabled = en;
}
chkSaturation.onClick = updateEnabled;
updateEnabled();
// ボタン
var btnGroup = dlg.add('group');
btnGroup.alignment = 'right';
var okBtn = btnGroup.add('button', undefined, 'OK', {
name: 'ok'
});
var cancelBtn = btnGroup.add('button', undefined, 'キャンセル', {
name: 'cancel'
});
var result = {
ok: false,
enableKTaper: false,
kReduceStart: K_REDUCE_START,
kReduceEnd: K_REDUCE_END
};
var ret = dlg.show();
if (ret !== 1) {
return result; // ok=false のまま返す
}
result.ok = true;
result.enableKTaper = chkSaturation.value;
if (ddK.selection) {
var v = parseInt(ddK.selection.text, 10);
if (!isNaN(v)) {
result.kReduceStart = v;
}
}
if (ddKEnd.selection) {
var v2 = parseInt(ddKEnd.selection.text, 10);
if (!isNaN(v2)) {
result.kReduceEnd = v2;
}
}
return result;
}
/////////////////////////////////////////
// ユーティリティ関数
/////////////////////////////////////////
// Lab値を格子キーに変換
function labKeyFrom(L, a, b) {
var Li = Math.round(L / QL);
var Ai = Math.round(a / QA);
var Bi = Math.round(b / QB);
return Li + '|' + Ai + '|' + Bi;
}
// 現在時刻をミリ秒で返す(処理時間計測用)
function nowMs() {
return (new Date()).getTime();
}
// 数値を小数第2位で丸め(表示用)
function round2(x) {
return Math.round(x * 100) / 100;
}
// === 入力CMYK→出力CMYK キャッシュ(丸めはキャッシュ専用: 小数1桁) ===
var resultCache = {};
var cacheHitCount = 0;
function round1(v) {
return Math.round(v * 10) / 10;
}
function cmykKey1(c, m, y, k) {
// 小数1桁で固定文字列化(キャッシュ専用)
return round1(c).toFixed(1) + "," + round1(m).toFixed(1) + "," + round1(y).toFixed(1) + "," + round1(k).toFixed(1);
}
function cacheGetResult(cmyk) {
var key = cmykKey1(cmyk.c, cmyk.m, cmyk.y, cmyk.k);
var v = resultCache[key];
return v ? {
c: v.c,
m: v.m,
y: v.y,
k: v.k
} : null;
}
function cachePutResult(inCmyk, outCmyk) {
var key = cmykKey1(inCmyk.c, inCmyk.m, inCmyk.y, inCmyk.k);
var norm = intCMYK(outCmyk); // 閾値適用前の整数化のみを保存
resultCache[key] = {
c: norm.c,
m: norm.m,
y: norm.y,
k: norm.k
};
}
// === CMYK→Lab メモ化(キャッシュ専用丸めは小数1桁) ===
var labConvCache = {};
// === Spot(グローバルCMYK)の処理済み管理 ===
var processedSpotMap = {};
function spotKey(spot) {
// 優先: index が取れる場合はそれを使う
try {
if (spot.hasOwnProperty('index')) return 'idx:' + spot.index;
} catch (e) { }
// 次善: toString([Spot Spot N] 等)
try {
var s = spot.toString();
if (s) return 'obj:' + s;
} catch (e) {
// 代替: 名前(自動リネーム対策として旧名も残す運用。後段で新名も登録する)
try {
if (spot.name) return 'name:' + spot.name;
} catch (e) { }
}
return 'spot:unknown';
}
function cmykLabCacheGet(c, m, y, k) {
var key = cmykKey1(c, m, y, k);
var v = labConvCache[key];
return v ? {
L: v.L,
a: v.a,
b: v.b
} : null;
}
function cmykLabCachePut(c, m, y, k, lab) {
var key = cmykKey1(c, m, y, k);
labConvCache[key] = {
L: lab.L,
a: lab.a,
b: lab.b
};
}
function cmykToLab(c, m, y, k) {
// キャッシュ(小数1桁キー)を先に確認
var cached = cmykLabCacheGet(c, m, y, k);
if (cached) {
return {
L: cached.L,
a: cached.a,
b: cached.b
};
}
var lab = app.convertSampleColor(
ImageColorSpace.CMYK,
[c, m, y, k],
ImageColorSpace.LAB,
ColorConvertPurpose.defaultpurpose
);
var out = {
L: lab[0],
a: lab[1],
b: lab[2]
};
cmykLabCachePut(c, m, y, k, out);
return out;
}
// ステップリストでループ
function forStep(fn) {
for (var i = 0; i < STEPS.length; i++) fn(STEPS[i]);
}
// ΔE76: Lab間のユークリッド距離を計算(色差評価)
function de76(L1, a1, b1, L2, a2, b2) {
var dL = L1 - L2,
da = a1 - a2,
db = b1 - b2;
return Math.sqrt(dL * dL + da * da + db * db);
}
// CMYKColor オブジェクトを生成
function makeCMYK(c, m, y, k) {
var cc = new CMYKColor();
cc.cyan = c;
cc.magenta = m;
cc.yellow = y;
cc.black = k;
return cc;
}
// Illustrator Color から {c,m,y,k} を取得(CMYKColor/SpotColor(グローバルCMYK)対応)
function extractCMYK(color) {
if (!color) return null;
// SpotColor(ベースがCMYKのグローバルプロセスのみ対象。真の特色や非CMYKは除外)
if (color.typename === 'SpotColor') {
try {
var sp = color.spot; // Spot オブジェクト
// 真の特色(ColorModel.SPOT)はスキップ。グローバルプロセス(ColorModel.PROCESS)のみ対象。
if (sp && sp.colorType === ColorModel.PROCESS && sp.color && sp.color.typename === 'CMYKColor') {
return {
c: sp.color.cyan,
m: sp.color.magenta,
y: sp.color.yellow,
k: sp.color.black,
_isSpot: true,
_spotRef: sp
};
}
} catch (e) { }
// SpotColor だが対象外(真の特色や非CMYKベース)は null
return null;
}
if (color.typename === 'CMYKColor') return {
c: color.cyan,
m: color.magenta,
y: color.yellow,
k: color.black
};
return null; // Spot/Gray/RGB/Pattern/Gradient は対象外(Gradientは別処理)
}
// CMYK値を整数に丸め
function intCMYK(cmyk) {
return {
c: Math.round(cmyk.c),
m: Math.round(cmyk.m),
y: Math.round(cmyk.y),
k: Math.round(cmyk.k)
};
}
// CMYK値の確定処理(閾値適用→整数化)
function finalizeCMYK(cmyk) {
function f(v) {
var r = Math.round(v); // 先に四捨五入(整数化)
if (r <= ZERO_THR) return 0; // 閾値は最後に適用
if (r < 0) r = 0;
if (r > 100) r = 100;
return r;
}
return {
c: f(cmyk.c),
m: f(cmyk.m),
y: f(cmyk.y),
k: f(cmyk.k)
};
}
// 共通: 動的半径で候補を収集しΔE昇順で返す(重複除去・フォールバック付き)
function collectCandidatesDynamic(labIndex, Lq, aq, bq, rCap, rHardCap, cap) {
if (rCap == null) rCap = 3;
if (rHardCap == null) rHardCap = Math.max(rCap, 6);
if (cap == null) cap = 64;
var Li = Math.round(Lq / QL),
Ai = Math.round(aq / QA),
Bi = Math.round(bq / QB);
var found = [],
seen = {};
function pushCandidate(t) {
var key = t.c + "," + t.m + "," + t.y + "," + t.k;
if (seen[key]) return;
seen[key] = true;
t.dE = de76(Lq, aq, bq, t.L, t.a, t.b);
found.push(t);
}
// 楕円体(ΔE半径R)内のみを走査
function collectAtLabRadius(R) {
var rL = Math.ceil(R / QL);
var rA = Math.ceil(R / QA);
var rB = Math.ceil(R / QB);
var R2 = R * R;
for (var dL = -rL; dL <= rL; dL++) {
var dL2 = (dL * QL) * (dL * QL);
for (var dA = -rA; dA <= rA; dA++) {
var dA2 = (dA * QA) * (dA * QA);
for (var dB = -rB; dB <= rB; dB++) {
var de2 = dL2 + dA2 + (dB * QB) * (dB * QB);
if (de2 > R2) continue; // 楕円体外はスキップ
var key = (Li + dL) + '|' + (Ai + dA) + '|' + (Bi + dB);
var arr = labIndex[key];
if (!arr) continue;
for (var j = 0; j < arr.length && found.length < cap; j++) {
pushCandidate(arr[j]);
}
if (found.length >= cap) return; // 早期停止
}
}
}
}
// RはLab距離。初期値/刻みは定数、上限は呼び出し引数に従う
var Rmin = Math.max(0, Math.min(R_START, rCap));
var R = Rmin;
while (R <= rCap && found.length < cap) {
collectAtLabRadius(R);
R += R_STEP;
}
while (R <= rHardCap && found.length < cap) {
collectAtLabRadius(R);
R += R_STEP;
}
if (found.length === 0) {
var Rpad = rHardCap + R_STEP;
var Rlimit = rHardCap + R_PAD_MAX;
while (Rpad <= Rlimit && found.length < cap) {
collectAtLabRadius(Rpad);
Rpad += R_STEP;
}
}
found.sort(function (a, b) {
return a.dE - b.dE;
});
return found;
}
// -------- k近傍(固定点数)取得:動的半径拡張でk件を確保、候補なければフォールバック --------
function kNearestFromIndex(labIndex, Lq, aq, bq, k, rMax) {
if (k == null) k = 8;
var rCap = (rMax != null ? Math.max(rMax, 3) : 3);
var rHardCap = Math.max(rCap, 6);
var arr = collectCandidatesDynamic(labIndex, Lq, aq, bq, rCap, rHardCap, k);
return (arr.length > 0) ? arr.slice(0, k) : arr;
}
// -------- 最終形パターンの許可マスク(Kは常に可動) --------
function allowedMaskForPattern(pattern) {
return {
c: (pattern === 'C' || pattern === 'CM' || pattern === 'YC'),
m: (pattern === 'M' || pattern === 'CM' || pattern === 'MY'),
y: (pattern === 'Y' || pattern === 'MY' || pattern === 'YC'),
k: true
};
}
// -------- 距離重み(IDW)でCMYK合成(許可外チャネルは0固定) --------
function idwBlendCMYKWithMask(samples, allowed) {
var eps = 1e-6,
p = 2.0;
var wsum = 0,
wc = 0,
wm = 0,
wy = 0,
wk = 0;
for (var i = 0; i < samples.length; i++) {
var s = samples[i];
var d = Math.max(eps, s.dE);
var w = 1 / Math.pow(d, p);
wsum += w;
if (allowed.c) wc += w * s.c;
if (allowed.m) wm += w * s.m;
if (allowed.y) wy += w * s.y;
if (allowed.k) wk += w * s.k;
}
return {
c: allowed.c ? Math.max(0, Math.min(100, wc / wsum)) : 0,
m: allowed.m ? Math.max(0, Math.min(100, wm / wsum)) : 0,
y: allowed.y ? Math.max(0, Math.min(100, wy / wsum)) : 0,
k: allowed.k ? Math.max(0, Math.min(100, wk / wsum)) : 0
};
}
// ==== 小行列ソルバ&LS微調整(K-onlyで一気にLを合わせる) ====
function _solveSymmetric(JTJ, b) {
var n = JTJ.length;
var M = new Array(n);
for (var i = 0; i < n; i++) {
M[i] = JTJ[i].slice();
M[i].push(b[i]);
}
for (var k = 0; k < n; k++) {
var piv = Math.abs(M[k][k]),
pr = k;
for (var r = k + 1; r < n; r++) {
var v = Math.abs(M[r][k]);
if (v > piv) {
piv = v;
pr = r;
}
}
if (piv < 1e-12) return null;
if (pr != k) {
var t = M[k];
M[k] = M[pr];
M[pr] = t;
}
var div = M[k][k];
for (var j = k; j <= n; j++) M[k][j] /= div;
for (var i2 = 0; i2 < n; i2++) {
if (i2 === k) continue;
var f = M[i2][k];
for (var j2 = k; j2 <= n; j2++) M[i2][j2] -= f * M[k][j2];
}
}
var x = new Array(n);
for (var i3 = 0; i3 < n; i3++) x[i3] = M[i3][n];
return x;
}
var _LS_H = 1.0; // 数値ヤコビアンの微小ステップ(%)
var _LS_LAMBDA = 0.1; // 正則化
function refineWithMaskLS(start, targetLab, allowed) {
var cur = {
c: start.c,
m: start.m,
y: start.y,
k: start.k
};
var lab = cmykToLab(cur.c, cur.m, cur.y, cur.k);
// 有効チャネルを列順に並べる
var cols = [];
var names = ['c', 'm', 'y', 'k'];
for (var i = 0; i < 4; i++) {
var ch = names[i];
if (allowed[ch]) cols.push(ch);
}
var p = cols.length;
if (p === 0) return {
c: cur.c,
m: cur.m,
y: cur.y,
k: cur.k,
L: lab.L,
a: lab.a,
b: lab.b,
dE: de76(targetLab.L, targetLab.a, targetLab.b, lab.L, lab.a, lab.b)
};
// ヤコビアン J: 3 x p(前進差分)
var J = new Array(3);
for (var r = 0; r < 3; r++) {
J[r] = new Array(p);
for (var c = 0; c < p; c++) J[r][c] = 0;
}
for (var ci = 0; ci < p; ci++) {
var ch = cols[ci];
var h = _LS_H;
var trial = {
c: cur.c,
m: cur.m,
y: cur.y,
k: cur.k
};
trial[ch] = Math.max(0, Math.min(100, trial[ch] + h));
var lab2 = cmykToLab(trial.c, trial.m, trial.y, trial.k);
J[0][ci] = (lab2.L - lab.L) / h;
J[1][ci] = (lab2.a - lab.a) / h;
J[2][ci] = (lab2.b - lab.b) / h;
}
// 正規方程式 A = J^T J + λI, b = J^T (target-lab)
var A = new Array(p),
bvec = new Array(p);
for (var i2 = 0; i2 < p; i2++) {
A[i2] = new Array(p);
for (var j2 = 0; j2 < p; j2++) {
var s = 0;
for (var r2 = 0; r2 < 3; r2++) s += J[r2][i2] * J[r2][j2];
A[i2][j2] = s;
}
A[i2][i2] += _LS_LAMBDA;
}
var tL = targetLab.L - lab.L,
ta = targetLab.a - lab.a,
tb = targetLab.b - lab.b;
for (var i4 = 0; i4 < p; i4++) {
bvec[i4] = J[0][i4] * tL + J[1][i4] * ta + J[2][i4] * tb;
}
var dx = _solveSymmetric(A, bvec);
if (dx) {
for (var ci2 = 0; ci2 < p; ci2++) {
var ch2 = cols[ci2];
cur[ch2] = Math.max(0, Math.min(100, cur[ch2] + dx[ci2]));
}
lab = cmykToLab(cur.c, cur.m, cur.y, cur.k);
}
var dEfinal = de76(targetLab.L, targetLab.a, targetLab.b, lab.L, lab.a, lab.b);
return {
c: cur.c,
m: cur.m,
y: cur.y,
k: cur.k,
L: lab.L,
a: lab.a,
b: lab.b,
dE: dEfinal
};
}
// -------- パターン許可での局所微調整(±REFINE_STEP) --------
function refineWithMask(start, targetLab, allowed) {
var best = {
c: start.c,
m: start.m,
y: start.y,
k: start.k
};
var bestLab = cmykToLab(best.c, best.m, best.y, best.k);
var bestDe = de76(targetLab.L, targetLab.a, targetLab.b, bestLab.L, bestLab.a, bestLab.b);
for (var it = 0; it < MAX_REFINE_ITERS; it++) {
var improved = false;
var chans = ['c', 'm', 'y', 'k'];
for (var ci = 0; ci < chans.length; ci++) {
var ch = chans[ci];
if (!allowed[ch]) continue;
var deltas = [REFINE_STEP, -REFINE_STEP];
for (var di = 0; di < deltas.length; di++) {
var cand = {
c: best.c,
m: best.m,
y: best.y,
k: best.k
};
cand[ch] = Math.max(0, Math.min(100, cand[ch] + deltas[di]));
var lab = cmykToLab(cand.c, cand.m, cand.y, cand.k);
var dE = de76(targetLab.L, targetLab.a, targetLab.b, lab.L, lab.a, lab.b);
if (dE + 1e-9 < bestDe) {
best = cand;
bestLab = lab;
bestDe = dE;
improved = true;
}
}
}
if (!improved) break;
}
return {
c: best.c,
m: best.m,
y: best.y,
k: best.k,
L: bestLab.L,
a: bestLab.a,
b: bestLab.b,
dE: bestDe
};
}
// -------- K削減 + CMY再フィット(Labを維持するための後処理) --------
function taperKAndRefineCMY(adj, targetLab) {
// adj: {c,m,y,k,(L,a,b,dE)} / targetLab: {L,a,b}
if (!adj || !targetLab) return adj;
// 元の値をコピー
var out = {
c: adj.c,
m: adj.m,
y: adj.y,
k: adj.k
};
// K削減全体が無効なら何もしない
if (!K_TAPER_ENABLED) {
return adj;
}
// CMY成分がほぼゼロ(実質Kのみ)の場合は、K削減は行わず元の値を返す
// 「CMYがなくKのみ」を ZERO_THR 以下で判定
if (out.c <= ZERO_THR && out.m <= ZERO_THR && out.y <= ZERO_THR) {
if (adj.L == null || adj.a == null || adj.b == null) {
var labPureK = cmykToLab(out.c, out.m, out.y, out.k);
adj.L = labPureK.L;
adj.a = labPureK.a;
adj.b = labPureK.b;
}
return adj;
}
var k0 = out.k;
// しきい値に応じてKのみを変形
if (k0 <= K_REDUCE_START) {
// ごく浅いKは完全に0へ
out.k = 0;
} else if (k0 >= K_REDUCE_END) {
// 深いKはそのまま
out.k = k0;
} else {
// K_REDUCE_START〜K_REDUCE_END の間を滑らかに圧縮
// k0 = K_REDUCE_START で 0, K_REDUCE_END で元のk0 になるように線形補間
var w = (k0 - K_REDUCE_START) / (K_REDUCE_END - K_REDUCE_START);
if (w < 0) w = 0;
if (w > 1) w = 1;
out.k = k0 * w;
}
// Kがほぼ変化していなければ、そのまま返す
if (Math.abs(out.k - k0) < 0.01) {
// adj が Lab を持っていなければ埋めておく
if (adj.L == null || adj.a == null || adj.b == null) {
var lab0 = cmykToLab(out.c, out.m, out.y, out.k);
adj.L = lab0.L;
adj.a = lab0.a;
adj.b = lab0.b;
}
return adj;
}
// CMY のみ自由にして、Labを targetLab に近付ける
// ただし、元々 0(または ZERO_THR 以下)だった CMY チャネルは固定して新たに色を立てない
var allowed = {
c: (out.c > ZERO_THR),
m: (out.m > ZERO_THR),
y: (out.y > ZERO_THR),
k: false
};
var res = refineWithMaskLS(out, targetLab, allowed);
if (!res) {
// LSが解けなかった場合は、out を元にLabを再計算して返す
var lab = cmykToLab(out.c, out.m, out.y, out.k);
return {
c: out.c,
m: out.m,
y: out.y,
k: out.k,
L: lab.L,
a: lab.a,
b: lab.b,
dE: de76(targetLab.L, targetLab.a, targetLab.b, lab.L, lab.a, lab.b)
};
}
return res;
}
// -------- 高位:Lab→CMYK(k近傍→IDW→パターン整形→微調整) --------
function adjustFromMapPatterned(labIndex, targetLab, k) {
if (k == null) k = 8;
// 低L & 低|ab| は K-only で早期確定
try {
var ab = Math.sqrt(targetLab.a * targetLab.a + targetLab.b * targetLab.b);
if (ab <= AB_TOL && targetLab.L <= getLBlack() + L_MARGIN) {
var allowedK = {
c: false,
m: false,
y: false,
k: true
};
var seed = {
c: 0,
m: 0,
y: 0,
k: 50
};
var resK = refineWithMaskLS(seed, targetLab, allowedK);
return resK;
}
} catch (e) { }
var kNN = kNearestFromIndex(labIndex, targetLab.L, targetLab.a, targetLab.b, k, 3);
if (!kNN || kNN.length === 0) return null;
var patterns = ['C', 'M', 'Y', 'CM', 'MY', 'YC'];
var best = null,
bestDe = 1e9;
for (var pi = 0; pi < patterns.length; pi++) {
var p = patterns[pi];
var allowed = allowedMaskForPattern(p);
// まず、同パターンのサンプルを優先的に利用
var samples = [];
for (var i = 0; i < kNN.length; i++) {
if (kNN[i].pattern === p) samples.push(kNN[i]);
}
if (samples.length === 0) samples = kNN; // 無ければ全体で代用
// IDW 合成 → パターン整形 → 許可チャネルで微調整
var init = idwBlendCMYKWithMask(samples, allowed);
var res = refineWithMask(init, targetLab, allowed);
if (res && res.dE < bestDe) {
bestDe = res.dE;
best = res;
}
}
return best;
}
// 単一カラー(CMYK/SpotColor)を処理 - 詳細な結果を返す
function processColorObject(color) {
var orig = extractCMYK(color);
if (!orig) return {
changed: false,
reason: 'nonCMYK'
}; // spot/gray/RGB等
// Spot(グローバルCMYK)の場合は、スウォッチ(Spot)のベース色を書き換える。オブジェクト側は触らない。
if (orig._isSpot && orig._spotRef) {
var sp = orig._spotRef;
var key = spotKey(sp);
if (processedSpotMap[key]) {
return {
changed: false,
reason: 'spotAlreadyProcessed'
};
}
// 変換
var labA = cmykToLab(orig.c, orig.m, orig.y, orig.k);
if (!gLabIndex) return {
changed: false,
reason: 'noIndex'
};
var adj = adjustFromMapPatterned(gLabIndex, labA, 8);
if (!adj) return {
changed: false,
reason: 'noCandidate'
};
// K削減オプションが有効なら、ここで後処理を適用
if (K_TAPER_ENABLED && adj.k < K_REDUCE_END) {
adj = taperKAndRefineCMY(adj, labA);
}
var norm = intCMYK({
c: adj.c,
m: adj.m,
y: adj.y,
k: adj.k
});
var finalOut = finalizeCMYK(norm);
try {
// Spot のベースカラーを書き換え(これで同スウォッチ適用先が一括更新される)
sp.color = makeCMYK(finalOut.c, finalOut.m, finalOut.y, finalOut.k);
} catch (e) {
return {
changed: false,
reason: 'spotWriteFailed'
};
}
// 自動リネーム対策:新しいキーも登録
processedSpotMap[key] = true;
try {
processedSpotMap[spotKey(sp)] = true;
} catch (e) { }
return {
changed: true,
out: finalOut,
spot: true
};
}
// まずキャッシュ(小数1桁キー)を確認
var cached = cacheGetResult(orig);
if (cached) {
cacheHitCount++;
// キャッシュには整数化のみ(非閾値)が入っている → 最後に閾値を適用
var finalFromCache = finalizeCMYK({
c: cached.c,
m: cached.m,
y: cached.y,
k: cached.k
});
if (orig.c === finalFromCache.c && orig.m === finalFromCache.m && orig.y === finalFromCache.y && orig.k === finalFromCache.k)
return {
changed: false,
reason: 'same'
};
return {
changed: true,
out: finalFromCache
};
}
var labA = cmykToLab(orig.c, orig.m, orig.y, orig.k);
if (!gLabIndex) return {
changed: false,
reason: 'noIndex'
};
var adj = adjustFromMapPatterned(gLabIndex, labA, 8);
if (!adj) return {
changed: false,
reason: 'noCandidate'
};
// K削減オプションが有効なら、ここで後処理を適用
if (K_TAPER_ENABLED && adj.k < K_REDUCE_END) {
adj = taperKAndRefineCMY(adj, labA);
}
// 二段後処理: まず整数化のみ → キャッシュ保存、適用直前に閾値
var norm = intCMYK({
c: adj.c,
m: adj.m,
y: adj.y,
k: adj.k
});
cachePutResult(orig, norm); // 閾値適用前の整数値を保存
var finalOut = finalizeCMYK(norm); // 閾値は最後に適用
// 厳密一致のみを"同一"とみなす
if (orig.c === finalOut.c && orig.m === finalOut.m && orig.y === finalOut.y && orig.k === finalOut.k)
return {
changed: false,
reason: 'same'
};
return {
changed: true,
out: finalOut
};
}
// TextFrame の文字カラー処理(塗り/線): 実務で使用する統計を返す
function processTextFrameColors(tf) {
var changes = 0;
try {
var attrs = tf.textRange.characterAttributes;
if (attrs) {
try {
var fc = attrs.fillColor;
if (fc) {
var r1 = processColorObject(fc);
if (r1 && r1.changed) {
if (!r1.spot) {
attrs.fillColor = makeCMYK(r1.out.c, r1.out.m, r1.out.y, r1.out.k);
}
changes++;
}
if (gProgress) gProgress.step(1);
}
} catch (e) { }
try {
var sc = attrs.strokeColor;
if (sc) {
var r2 = processColorObject(sc);
if (r2 && r2.changed) {
if (!r2.spot) {
attrs.strokeColor = makeCMYK(r2.out.c, r2.out.m, r2.out.y, r2.out.k);
}
changes++;
}
if (gProgress) gProgress.step(1);
}
} catch (e) { }
}
} catch (e) { }
return {
changes: changes
};
}
// GradientColor を処理(各ストップの CMYK/SpotColor) - カウンタ集計
function processGradientColor(gradColor) {
var g = gradColor.gradient;
var stops = g.gradientStops;
var changed = 0;
for (var i = 0; i < stops.length; i++) {
var col = stops[i].color;
if (col) {
var res = processColorObject(col);
if (res && res.changed) {
// Spot の場合はスウォッチ側が更新されているため stop の色は触らない
if (!res.spot) {
stops[i].color = makeCMYK(res.out.c, res.out.m, res.out.y, res.out.k);
}
changed++;
}
}
if (gProgress) gProgress.step(1);
}
return {
changed: changed
};
}
// PageItem の塗り・線を処理 - 詳細カウンタを返す
function processPageItemColors(item) {
var changes = 0;
try {
if (item.filled) {
var fc = item.fillColor;
if (fc && fc.typename === 'GradientColor') {
var r = processGradientColor(fc);
item.fillColor = fc;
changes += r.changed;
} else {
var r1 = processColorObject(fc);
if (r1 && r1.changed) {
if (!r1.spot) {
item.fillColor = makeCMYK(r1.out.c, r1.out.m, r1.out.y, r1.out.k);
}
changes++;
}
if (gProgress) gProgress.step(1);
}
}
} catch (e) { }
try {
if (item.stroked) {
var sc = item.strokeColor;
if (sc && sc.typename === 'GradientColor') {
var r2 = processGradientColor(sc);
item.strokeColor = sc;
changes += r2.changed;
} else {
var r3 = processColorObject(sc);
if (r3 && r3.changed) {
if (!r3.spot) {
item.strokeColor = makeCMYK(r3.out.c, r3.out.m, r3.out.y, r3.out.k);
}
changes++;
}
if (gProgress) gProgress.step(1);
}
}
} catch (e) { }
return {
changes: changes
};
}
// 選択を走査して全適用(Group/Compound含む) - 詳細集計
function applyToSelection() {
var doc = app.activeDocument;
var sel = doc.selection;
if (!sel) return {
count: 0
};
var applied = 0;
function walk(it) {
if (!it) return;
var tn = it.typename;
if (tn === 'GroupItem') {
var arr = it.pageItems;
for (var i = 0; i < arr.length; i++) walk(arr[i]);
} else if (tn === 'CompoundPathItem') {
var arr2 = it.pathItems;
for (var j = 0; j < arr2.length; j++) {
var r = processPageItemColors(arr2[j]);
applied += r.changes;
}
} else if (tn === 'TextFrame') {
var rt = processTextFrameColors(it);
applied += rt.changes;
} else if (tn === 'PathItem' || tn === 'MeshItem') {
var r3 = processPageItemColors(it);
applied += r3.changes;
}
}
for (var i = 0; i < sel.length; i++) walk(sel[i]);
return {
count: applied
};
}
/////////////////////////////////////////
// 適用フェーズの試行単位(塗り/線/グラデーションストップ)を概算カウント
function estimatePlannedAttempts() {
var doc = app.activeDocument;
var sel = doc.selection;
if (!sel || sel.length === 0) return 0;
var cnt = 0;
function countItem(it) {
if (!it) return;
var tn = it.typename;
if (tn === 'GroupItem') {
var arr = it.pageItems;
for (var i = 0; i < arr.length; i++) countItem(arr[i]);
return;
}
if (tn === 'CompoundPathItem') {
var arr2 = it.pathItems;
for (var j = 0; j < arr2.length; j++) countItem(arr2[j]);
return;
}
if (tn === 'TextFrame') {
try {
var at = it.textRange.characterAttributes;
if (at) {
try {
if (at.fillColor) cnt += 1;
} catch (e) { }
try {
if (at.strokeColor) cnt += 1;
} catch (e) { }
}
} catch (e) { }
return;
}
// PathItem / MeshItem など
try {
if (it.filled) {
var fc = it.fillColor;
if (fc && fc.typename === 'GradientColor') {
try {
cnt += fc.gradient.gradientStops.length;
} catch (e) {
cnt += 1;
}
} else {
cnt += 1;
}
}
} catch (e) { }
try {
if (it.stroked) {
var sc = it.strokeColor;
if (sc && sc.typename === 'GradientColor') {
try {
cnt += sc.gradient.gradientStops.length;
} catch (e) {
cnt += 1;
}
} else {
cnt += 1;
}
}
} catch (e) { }
}
for (var k = 0; k < sel.length; k++) countItem(sel[k]);
return cnt;
}
/////////////////////////////////////////
// メイン処理関数
/////////////////////////////////////////
// マップを生成し、計測と保持を行う処理
(
function main() {
var t0 = nowMs();
var totalPoints = 0;
// ドキュメントの色空間チェック(CMYK以外は中止)
try {
if (app.documents.length === 0) {
alert("CMYKドキュメントで実行してください");
return;
}
var _doc = app.activeDocument;
if (_doc.documentColorSpace !== DocumentColorSpace.CMYK) {
alert("CMYKドキュメントで実行してください");
return;
}
} catch (e) {
alert("CMYKドキュメントで実行してください");
return;
}
// スクリプト実行中の黒点L*値を確定させる
try {
getLBlack();
} catch (e) { }
// 対象パス数をカウントし、「変換オプション」ダイアログを表示
var planned = 0;
try {
planned = estimatePlannedAttempts();
} catch (e) { }
var opt = showConvertOptionsDialog(planned);
if (!opt || !opt.ok) {
// ユーザーがキャンセルした場合は処理を中止
return;
}
K_TAPER_ENABLED = opt.enableKTaper;
K_REDUCE_START = opt.kReduceStart;
K_REDUCE_END = opt.kReduceEnd;
// 準備中ダイアログ(全処理の冒頭で表示)
var prepWin = null;
try {
prepWin = new Window('palette', '準備中 ' + YamoScriptVersion, undefined, {
closeButton: false
});
var st0 = prepWin.add('statictext', undefined, 'オブジェクトカウントとLab色域マップを準備中...');
st0.preferredSize = [400, 20];
st0.justify = 'center';
prepWin.layout.layout(true);
prepWin.center();
prepWin.show();
try {
prepWin.update();
} catch (e) { }
try {
$.sleep(50);
} catch (e) { }
} catch (e) { }
// Lab→CMYK 逆引き用の格子ハッシュ
var labIndex = {}; // key: "Li|Ai|Bi" -> [{c,m,y,k,L,a,b,pattern}]
// C単色+Kを走査
forStep(function (K) {
forStep(function (C) {
try {
var lab = cmykToLab(C, 0, 0, K);
var key = labKeyFrom(lab.L, lab.a, lab.b);
if (!labIndex[key]) labIndex[key] = [];
labIndex[key].push({
c: C,
m: 0,
y: 0,
k: K,
L: lab.L,
a: lab.a,
b: lab.b,
pattern: 'C'
});
totalPoints++;
} catch (e) { }
});
});
// M単色+Kを走査
forStep(function (K) {
forStep(function (M) {
try {
var lab = cmykToLab(0, M, 0, K);
var key = labKeyFrom(lab.L, lab.a, lab.b);
if (!labIndex[key]) labIndex[key] = [];
labIndex[key].push({
c: 0,
m: M,
y: 0,
k: K,
L: lab.L,
a: lab.a,
b: lab.b,
pattern: 'M'
});
totalPoints++;
} catch (e) { }
});
});
// Y単色+Kを走査
forStep(function (K) {
forStep(function (Y) {
try {
var lab = cmykToLab(0, 0, Y, K);
var key = labKeyFrom(lab.L, lab.a, lab.b);
if (!labIndex[key]) labIndex[key] = [];
labIndex[key].push({
c: 0,
m: 0,
y: Y,
k: K,
L: lab.L,
a: lab.a,
b: lab.b,
pattern: 'Y'
});
totalPoints++;
} catch (e) { }
});
});
// CM二色+Kを走査
forStep(function (K) {
forStep(function (C) {
forStep(function (M) {
try {
var lab = cmykToLab(C, M, 0, K);
var key = labKeyFrom(lab.L, lab.a, lab.b);
if (!labIndex[key]) labIndex[key] = [];
labIndex[key].push({
c: C,
m: M,
y: 0,
k: K,
L: lab.L,
a: lab.a,
b: lab.b,
pattern: 'CM'
});
totalPoints++;
} catch (e) { }
});
});
});
// MY二色+Kを走査
forStep(function (K) {
forStep(function (M) {
forStep(function (Y) {
try {
var lab = cmykToLab(0, M, Y, K);
var key = labKeyFrom(lab.L, lab.a, lab.b);
if (!labIndex[key]) labIndex[key] = [];
labIndex[key].push({
c: 0,
m: M,
y: Y,
k: K,
L: lab.L,
a: lab.a,
b: lab.b,
pattern: 'MY'
});
totalPoints++;
} catch (e) { }
});
});
});
// YC二色+Kを走査
forStep(function (K) {
forStep(function (Y) {
forStep(function (C) {
try {
var lab = cmykToLab(C, 0, Y, K);
var key = labKeyFrom(lab.L, lab.a, lab.b);
if (!labIndex[key]) labIndex[key] = [];
labIndex[key].push({
c: C,
m: 0,
y: Y,
k: K,
L: lab.L,
a: lab.a,
b: lab.b,
pattern: 'YC'
});
totalPoints++;
} catch (e) { }
});
});
});
// apply系ユーティリティから参照できるように公開
gLabIndex = labIndex;
// プログレスバー(適用フェーズのみ)開始:分母=塗り/線/ストップの概算
// ここでは変換オプションダイアログ表示時に算出した planned をそのまま使用
// 準備中ダイアログを閉じる
try {
if (prepWin) prepWin.close();
} catch (e) { }
gProgress = createProgressBar(planned > 0 ? planned : 100);
// 選択オブジェクトへ適用(色の置換)
var appliedInfo = applyToSelection();
var totalSec = (nowMs() - t0) / 1000.0; // スクリプト開始からの総時間
// 実行結果をダイアログ表示
var msg = [];
msg.push("完了");
msg.push("総点数: " + totalPoints);
msg.push("適用カラー数: " + appliedInfo.count);
msg.push("適用時間: " + round2(totalSec) + " 秒");
msg.push("cacheヒット: " + cacheHitCount);
if (gProgress) {
gProgress.close();
gProgress = null;
}
// ---- メモリ解放(実務向け)----
try {
// 大きいデータ構造の参照を切る
gLabIndex = null; // ラボインデックスのグローバル参照を破棄
resultCache = {}; // 入力→出力キャッシュをクリア
labConvCache = {}; // CMYK→Lab メモ化キャッシュをクリア
processedSpotMap = {}; // Spot処理済みマップもクリア
// カウンタ類も初期化(任意)
cacheHitCount = 0;
// GC を明示的に促す(ExtendScript)
$.gc();
} catch (e) { }
try {
app.redraw();
} catch (e) { }
alert(msg.join("\n"));
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment