|
/* |
|
<javascriptresource> |
|
<name>Illustratorに合わせてリサイズ</name> |
|
<category>YPresets</category> |
|
</javascriptresource> |
|
*/ |
|
|
|
var SCRIPT_VERSION = "Ver.1.5.1(2025-11-14)"; |
|
|
|
// ==== 拡大・縮小の警告しきい値 ==== |
|
var efScaleMin = 0.9; |
|
var efScaleMax = 1.1; |
|
var scaleMax = 2; |
|
|
|
// ==== ターゲット解像度候補 ==== |
|
var targetPPIList = [350, 400, 600, 1200]; |
|
|
|
// ==== CustomOptions(前回設定の保存/復元) ==== |
|
var PREF_ID = "com.yamo.psAiresize_v1"; |
|
var K_VER = stringIDToTypeID("version"); |
|
var K_RADIO_INDEX = stringIDToTypeID("radioIndex"); |
|
var K_UPSCALE = stringIDToTypeID("upscaleMethod"); |
|
var K_DOWNSCALE = stringIDToTypeID("downMethod"); |
|
var K_USE_PREV = stringIDToTypeID("usePrevSettings"); |
|
var SCHEMA_VERSION = 1; |
|
|
|
var defaultsPrefs = { |
|
usePrev: false, |
|
radioIndex: 0, |
|
upscaleMethod: "deepUpscale", |
|
downMethod: "bicubic" |
|
}; |
|
|
|
function cloneDefaultsPrefs() { |
|
return { |
|
usePrev: defaultsPrefs.usePrev, |
|
radioIndex: defaultsPrefs.radioIndex, |
|
upscaleMethod: defaultsPrefs.upscaleMethod, |
|
downMethod: defaultsPrefs.downMethod |
|
}; |
|
} |
|
|
|
function loadPrefsCO() { |
|
var prefs = cloneDefaultsPrefs(); |
|
try { |
|
var descriptor = app.getCustomOptions(PREF_ID); |
|
if (descriptor.hasKey(K_USE_PREV)) prefs.usePrev = descriptor.getBoolean(K_USE_PREV); |
|
if (descriptor.hasKey(K_RADIO_INDEX)) { |
|
var index = descriptor.getInteger(K_RADIO_INDEX); |
|
if (index >= 0 && index < targetPPIList.length) prefs.radioIndex = index; |
|
} |
|
if (descriptor.hasKey(K_UPSCALE)) prefs.upscaleMethod = descriptor.getString(K_UPSCALE); |
|
if (descriptor.hasKey(K_DOWNSCALE)) prefs.downMethod = descriptor.getString(K_DOWNSCALE); |
|
} catch (error) { |
|
// 初回起動時や設定破損時は既定値を使用 |
|
} |
|
return prefs; |
|
} |
|
|
|
function savePrefsCO(prefs) { |
|
var descriptor = new ActionDescriptor(); |
|
descriptor.putInteger(K_VER, SCHEMA_VERSION); |
|
descriptor.putBoolean(K_USE_PREV, !!prefs.usePrev); |
|
descriptor.putInteger(K_RADIO_INDEX, Math.max(0, Math.min(targetPPIList.length - 1, prefs.radioIndex))); |
|
descriptor.putString(K_UPSCALE, String(prefs.upscaleMethod)); |
|
descriptor.putString(K_DOWNSCALE, String(prefs.downMethod)); |
|
app.putCustomOptions(PREF_ID, descriptor, true); |
|
} |
|
|
|
// usePrev フラグのみを保存(他の設定値は維持) |
|
function saveUsePrevOnly(flag) { |
|
var currentPrefs = loadPrefsCO(); |
|
savePrefsCO({ |
|
usePrev: !!flag, |
|
radioIndex: currentPrefs.radioIndex, |
|
upscaleMethod: currentPrefs.upscaleMethod, |
|
downMethod: currentPrefs.downMethod |
|
}); |
|
} |
|
|
|
// ==== ブラウザでURLを開くヘルパー関数 ==== |
|
function openURLInBrowser(url) { |
|
if (!url) return; |
|
try { |
|
var os = $.os.toLowerCase(); |
|
if (os.indexOf("mac") >= 0) { |
|
app.system('/usr/bin/open "' + url + '"'); |
|
} else if (os.indexOf("win") >= 0) { |
|
app.system('cmd.exe /c start "" "' + url + '"'); |
|
} else { |
|
alert("未対応OSです。\n" + url); |
|
} |
|
} catch (error) { |
|
alert("ブラウザを開けませんでした。\n" + error); |
|
} |
|
} |
|
|
|
// ==== Illustrator側でアイテムを選択する関数(BridgeTalk経由) ==== |
|
function selectInIllustrator(handle) { |
|
function illustratorSelectByHandle(handle) { |
|
if (app.documents.length === 0) return; |
|
var doc = app.activeDocument; |
|
doc.selection = null; |
|
|
|
var targetItem = null; |
|
|
|
// 1) placedIndex によるダイレクト検索(最速) |
|
if (typeof handle.placedIndex === 'number' && handle.placedIndex >= 0 && handle.placedIndex < doc.placedItems.length) { |
|
try { |
|
targetItem = doc.placedItems[handle.placedIndex]; |
|
} catch (error) { |
|
targetItem = null; |
|
} |
|
} |
|
|
|
// 2) itemId による検索 |
|
if (!targetItem && typeof handle.itemId === 'number') { |
|
for (var i = 0; i < doc.placedItems.length; i++) { |
|
try { |
|
if (doc.placedItems[i].id === handle.itemId) { |
|
targetItem = doc.placedItems[i]; |
|
break; |
|
} |
|
} catch (error) {} |
|
} |
|
} |
|
|
|
// 3) fsName による検索(フォールバック) |
|
function canonFsAlready(filePath) { |
|
if (!filePath) return filePath; |
|
if ($.os.indexOf('Windows') >= 0) return String(filePath).split('/').join('\\').toLowerCase(); |
|
return String(filePath); |
|
} |
|
if (!targetItem && handle.pathFs) { |
|
var canonicalTargetPath = canonFsAlready(handle.pathFs); |
|
for (var j = 0; j < doc.placedItems.length; j++) { |
|
var item = doc.placedItems[j]; |
|
var itemFilePath = null; |
|
try { |
|
if (item.file && item.file.fsName) itemFilePath = item.file.fsName; |
|
} catch (error) { |
|
itemFilePath = null; |
|
} |
|
if (itemFilePath && canonFsAlready(itemFilePath) === canonicalTargetPath) { |
|
targetItem = item; |
|
break; |
|
} |
|
} |
|
} |
|
|
|
if (!targetItem) return; |
|
|
|
// 選択と中央表示 |
|
try { |
|
targetItem.selected = true; |
|
} catch (error) {} |
|
var visibleBounds = null; |
|
try { |
|
visibleBounds = targetItem.visibleBounds; |
|
} catch (error) { |
|
visibleBounds = null; |
|
} |
|
if (visibleBounds) { |
|
var centerX = (visibleBounds[0] + visibleBounds[2]) / 2; |
|
var centerY = (visibleBounds[1] + visibleBounds[3]) / 2; |
|
var view = doc.activeView; |
|
view.centerPoint = [centerX, centerY]; |
|
view.zoom = 1.0; |
|
} |
|
try { |
|
doc.activate(); |
|
} catch (error) {} |
|
try { |
|
app.redraw(); |
|
} catch (error) {} |
|
} |
|
|
|
var bridgeTalk = new BridgeTalk(); |
|
bridgeTalk.target = "illustrator"; |
|
bridgeTalk.body = "(" + |
|
"function(){" + |
|
illustratorSelectByHandle.toString() + "\n" + |
|
"illustratorSelectByHandle(" + |
|
"{itemId:" + (handle.itemId != null ? handle.itemId : "null") + "," + |
|
"placedIndex:" + (handle.placedIndex != null ? handle.placedIndex : "null") + "," + |
|
"pathFs:'" + String(handle.pathFs || '').replace(/\\/g, "\\\\").replace(/'/g, "\\'") + "'}" + |
|
");" + |
|
"}" + |
|
")();"; |
|
bridgeTalk.onError = function(errorEvent) { |
|
alert("Illustrator BridgeTalk Error: " + errorEvent.body); |
|
}; |
|
bridgeTalk.onResult = function(resultEvent) { |
|
try { |
|
BridgeTalk.bringToFront("illustrator"); |
|
} catch (error) {} |
|
}; |
|
bridgeTalk.send(); |
|
} |
|
|
|
// ==== メインエントリ ==== |
|
function main() { |
|
if (!app.documents.length) { |
|
alert("開いているドキュメントがありません。"); |
|
return; |
|
} |
|
var activeDoc = app.activeDocument; |
|
var imagePath = activeDoc.fullName.fsName; |
|
|
|
// BridgeTalk: Illustratorへ配置情報を問い合わせ |
|
var bridgeTalk = new BridgeTalk(); |
|
bridgeTalk.target = "illustrator"; |
|
bridgeTalk.body = "(" + illustratorSideForPT.toString() + ")(\"" + imagePath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\");"; |
|
|
|
// Illustrator応答処理 |
|
bridgeTalk.onResult = function(responseEvent) { |
|
var responseData = responseEvent.body; |
|
if (responseData == "null" || !responseData) { |
|
alert("Illustratorドキュメント内に一致するリンク画像が見つかりません。"); |
|
return; |
|
} |
|
|
|
// JSON応答のパース |
|
var parsedObject; |
|
var responseText = String(responseData || "").replace(/^\uFEFF/, "").replace(/^\s+|\s+$/g, ""); |
|
try { |
|
parsedObject = JSON.parse(responseText); |
|
} catch (error) { |
|
try { |
|
parsedObject = eval("(" + responseText + ")"); |
|
} catch (error) { |
|
alert("Illustrator応答の解析に失敗しました。\nraw: " + responseData); |
|
return; |
|
} |
|
} |
|
|
|
// Photoshopドキュメント情報 |
|
var documentWidthPixels = activeDoc.width.as("px"); |
|
var documentHeightPixels = activeDoc.height.as("px"); |
|
var longSidePixels = Math.max(documentWidthPixels, documentHeightPixels); |
|
|
|
// 配置アイテム配列の正規化 |
|
var placedItemsArray = (parsedObject && parsedObject.items && parsedObject.items.length) ? parsedObject.items : null; |
|
if (!placedItemsArray) { |
|
placedItemsArray = [{ |
|
longSidePt: parsedObject.longSidePt, |
|
linkStatus: parsedObject.linkStatus, |
|
itemId: (parsedObject && parsedObject.itemId != null) ? Number(parsedObject.itemId) : null, |
|
placedIndex: (parsedObject && parsedObject.placedIndex != null) ? Number(parsedObject.placedIndex) : null |
|
}]; |
|
} |
|
|
|
// リンク状態チェック |
|
for (var linkIndex = 0; linkIndex < placedItemsArray.length; linkIndex++) { |
|
var linkStatus = placedItemsArray[linkIndex].linkStatus; |
|
if (linkStatus === 1) { |
|
alert("リンク切れの画像が含まれています。処理を中止します。"); |
|
return; |
|
} |
|
if (linkStatus === 2) { |
|
alert("未更新のリンク画像が含まれています。処理を中止します。"); |
|
return; |
|
} |
|
} |
|
|
|
// 最大配置サイズと最小配置サイズを検索 |
|
var maxSizeIndex = 0, |
|
maxLongSidePt = -Infinity; |
|
var minSizeIndex = 0, |
|
minLongSidePt = Infinity; |
|
for (var itemIndex = 0; itemIndex < placedItemsArray.length; itemIndex++) { |
|
var longSidePtValue = Number(placedItemsArray[itemIndex].longSidePt) || 0; |
|
if (longSidePtValue > maxLongSidePt) { |
|
maxLongSidePt = longSidePtValue; |
|
maxSizeIndex = itemIndex; |
|
} |
|
if (longSidePtValue < minLongSidePt) { |
|
minLongSidePt = longSidePtValue; |
|
minSizeIndex = itemIndex; |
|
} |
|
} |
|
|
|
// 最小配置サイズのppi(最大ppi)を算出 |
|
var minLongSidePtValue = Number(placedItemsArray[minSizeIndex].longSidePt) || 0; |
|
var minLongSideMM = minLongSidePtValue * 25.4 / 72.0; |
|
var minSizePpi = (minLongSideMM > 0) ? (longSidePixels / (minLongSideMM / 25.4)) : 0; |
|
|
|
// 処理対象(最大配置サイズ)の情報を取得 |
|
var targetItem = placedItemsArray[maxSizeIndex]; |
|
var longSidePt = Number(targetItem.longSidePt) || 0; |
|
var illustratorItemId = (targetItem.itemId != null) ? Number(targetItem.itemId) : null; |
|
var illustratorPlacedIndex = (targetItem.placedIndex != null) ? Number(targetItem.placedIndex) : null; |
|
var longSideMM = longSidePt * 25.4 / 72.0; |
|
var placedPPI = (longSideMM > 0) ? (longSidePixels / (longSideMM / 25.4)) : 0; |
|
|
|
// ==== リサイズ確認ダイアログ ==== |
|
function showConfirmDialog(messageBase, placedPPI, imagePath) { |
|
var dialog = new Window("dialog", "画像リサイズ " + (typeof SCRIPT_VERSION !== 'undefined' ? SCRIPT_VERSION : "")); |
|
dialog.orientation = "column"; |
|
dialog.alignChildren = ["fill", "top"]; |
|
dialog.margins = 15; |
|
|
|
|
|
var infoPanel = dialog.add("panel", undefined, "画像情報"); |
|
infoPanel.orientation = "column"; |
|
infoPanel.alignChildren = ["fill", "top"]; |
|
infoPanel.margins = [10, 12, 10, 12]; |
|
infoPanel.spacing = 2; |
|
|
|
|
|
var separatorIndex = messageBase.indexOf("\n\n"); |
|
var headerText = (separatorIndex >= 0) ? messageBase.substring(0, separatorIndex) : messageBase; |
|
var tailText = (separatorIndex >= 0) ? messageBase.substring(separatorIndex + 2) : ""; |
|
|
|
|
|
var infoText = infoPanel.add("statictext", undefined, headerText, { |
|
multiline: true |
|
}); |
|
infoText.minimumSize.width = 400; |
|
|
|
if (tailText) { |
|
var messageText = infoPanel.add("statictext", undefined, tailText, { |
|
multiline: true |
|
}); |
|
messageText.minimumSize.width = 400; |
|
try { |
|
var font = messageText.graphics.font; |
|
messageText.graphics.font = ScriptUI.newFont(font.name, 'Bold', font.size); |
|
} catch (error) {} |
|
} |
|
|
|
// 指定解像度 |
|
var radioGroup = dialog.add("panel", undefined, "指定解像度"); |
|
radioGroup.orientation = "row"; |
|
radioGroup.alignChildren = ["left", "center"]; |
|
radioGroup.margins = [10, 12, 10, 12]; |
|
|
|
var radioButtons = []; |
|
for (var i = 0; i < targetPPIList.length; i++) { |
|
var radioButton = radioGroup.add('radiobutton', undefined, String(targetPPIList[i])); |
|
radioButtons.push(radioButton); |
|
} |
|
|
|
// 前回設定の読み込み |
|
var customOptionsPrefs = loadPrefsCO(); |
|
var initialRadioIndex = 0; |
|
var initialUpscaleMethod = "deepUpscale"; |
|
var initialDownscaleMethod = "bicubic"; |
|
if (customOptionsPrefs.usePrev) { |
|
initialRadioIndex = (customOptionsPrefs.radioIndex >= 0 && customOptionsPrefs.radioIndex < targetPPIList.length) ? customOptionsPrefs.radioIndex : 0; |
|
initialUpscaleMethod = customOptionsPrefs.upscaleMethod || initialUpscaleMethod; |
|
initialDownscaleMethod = customOptionsPrefs.downMethod || initialDownscaleMethod; |
|
} |
|
|
|
for (var radioButtonIndex = 0; radioButtonIndex < radioButtons.length; radioButtonIndex++) { |
|
radioButtons[radioButtonIndex].value = (radioButtonIndex === initialRadioIndex); |
|
} |
|
|
|
// 拡大メソッド |
|
var upscaleMethodPanel = dialog.add("panel", undefined, "拡大メソッド"); |
|
upscaleMethodPanel.orientation = "row"; |
|
upscaleMethodPanel.alignChildren = ["left", "center"]; |
|
upscaleMethodPanel.margins = [10, 12, 10, 12]; |
|
var upscaleMethodRadioButtons = []; |
|
var upscaleMethodLabels = [ |
|
"ディテールを保持2.0(推奨)", |
|
"ディテールを保持(旧)", |
|
"ニアレストネイバー" |
|
]; |
|
var upscaleMethodValues = [ |
|
"deepUpscale", |
|
"preserveDetails", |
|
"nearestNeighbor" |
|
]; |
|
for (var i = 0; i < upscaleMethodLabels.length; i++) { |
|
var upscaleRadioButton = upscaleMethodPanel.add('radiobutton', undefined, upscaleMethodLabels[i]); |
|
upscaleMethodRadioButtons.push(upscaleRadioButton); |
|
upscaleRadioButton.value = (upscaleMethodValues[i] === initialUpscaleMethod); |
|
} |
|
|
|
// 縮小メソッド選択 |
|
var downscaleMethodPanel = dialog.add("panel", undefined, "縮小メソッド"); |
|
downscaleMethodPanel.orientation = "row"; |
|
downscaleMethodPanel.alignChildren = ["left", "center"]; |
|
downscaleMethodPanel.margins = [10, 12, 10, 12]; |
|
var downscaleMethodRadioButtons = []; |
|
var downscaleMethodLabels = [ |
|
"バイキュービック(滑らか)", |
|
"ニアレストネイバー" |
|
]; |
|
var downscaleMethodValues = [ |
|
"bicubic", |
|
"nearestNeighbor" |
|
]; |
|
for (var i = 0; i < downscaleMethodLabels.length; i++) { |
|
var downscaleRadioButton = downscaleMethodPanel.add('radiobutton', undefined, downscaleMethodLabels[i]); |
|
downscaleMethodRadioButtons.push(downscaleRadioButton); |
|
downscaleRadioButton.value = (downscaleMethodValues[i] === initialDownscaleMethod); |
|
} |
|
|
|
// 拡縮率のリアルタイム表示 |
|
var scaleText = dialog.add("statictext", undefined, ""); |
|
scaleText.minimumSize.width = 400; |
|
try { |
|
var scaleFont = scaleText.graphics.font; |
|
scaleText.graphics.font = ScriptUI.newFont(scaleFont.name, 'Bold', scaleFont.size); |
|
} catch (error) {} |
|
|
|
// 警告テキスト(背景は変更せず、テキストのみ表示) |
|
var warningText = dialog.add("statictext", undefined, " ", { |
|
multiline: true |
|
}); |
|
warningText.preferredSize = [400, 80]; |
|
warningText.minimumSize = [400, 80]; |
|
warningText.alignment = ["fill", "top"]; |
|
try { |
|
warningText.graphics.font = ScriptUI.newFont(warningText.graphics.font.name, 'Bold', warningText.graphics.font.size); |
|
} catch (error) {} |
|
|
|
// 追加警告(白・ボールド、最下段に表示) |
|
var extraWarningText = dialog.add("statictext", undefined, " ", { |
|
multiline: true |
|
}); |
|
extraWarningText.preferredSize = [400, 40]; |
|
extraWarningText.minimumSize = [400, 40]; |
|
extraWarningText.alignment = ["fill", "top"]; |
|
try { |
|
var extraWarningFont = extraWarningText.graphics.font; |
|
extraWarningText.graphics.font = ScriptUI.newFont(extraWarningFont.name, 'Bold', extraWarningFont.size); |
|
} catch (error) {} |
|
|
|
|
|
dialog.preferredSize = [460, 360]; |
|
|
|
|
|
function updateUI() { |
|
var selectedPPI = null; |
|
for (var i = 0; i < radioButtons.length; i++) { |
|
if (radioButtons[i].value) { |
|
selectedPPI = parseInt(radioButtons[i].text); |
|
break; |
|
} |
|
} |
|
var scaleRatio = (selectedPPI / placedPPI); |
|
var scalePercent = scaleRatio * 100; |
|
scaleText.text = "拡縮率: " + scalePercent.toFixed(2) + " %"; |
|
|
|
// しきい値ベースの注意喚起(無駄拡縮/過大拡大)+ Bitmap専用注意 |
|
var warningMessage = ""; |
|
// Bitmap(2値)モードは最優先で注意喚起 |
|
try { |
|
if (activeDoc && activeDoc.mode == DocumentMode.BITMAP) { |
|
warningMessage = "【注意:キャンセル推奨】\n2値画像のリサイズは、モアレが発生しやすいため推奨しません。\nリサンプルする場合は2,400ppi以下、600ppi以上を選択してください。"; |
|
} |
|
} catch (error) {} |
|
// 既存のしきい値警告を付加 |
|
if (scaleRatio >= efScaleMin && scaleRatio <= efScaleMax) { |
|
warningMessage += (warningMessage ? "\n" : "") + "【注意:キャンセル推奨】\n拡大率が " + scalePercent.toFixed(2) + "% です。無駄な拡縮で、余計な画像劣化の可能性があります。"; |
|
} |
|
if (scaleRatio > scaleMax) { |
|
warningMessage += (warningMessage ? "\n" : "") + "【警告:キャンセルを推奨】\n 拡大率が " + (scaleMax * 100).toFixed(0) + "% を超えています。\nPhotoshop以外の手段を検討してください。"; |
|
} |
|
|
|
// 最少配置サイズのppiに対する逸脱判定(高すぎる場合) |
|
var extraWarningMessage = ""; |
|
try { |
|
if (typeof minSizePpi !== 'undefined' && isFinite(minSizePpi) && placedItemsArray && placedItemsArray.length > 1) { |
|
var minPlacementScale = selectedPPI / minSizePpi; // minSizePpi が大きいほど scale は小さくなる |
|
if (minPlacementScale < efScaleMin) { |
|
extraWarningMessage = "最小配置画像の実効解像度が高すぎます。画像ファイルを分けることを推奨します。"; |
|
} |
|
} |
|
} catch (error) {} |
|
|
|
warningText.text = warningMessage; |
|
var hasWarning = !!warningMessage; |
|
try { |
|
var warningPen = warningText.graphics.newPen(warningText.graphics.PenType.SOLID_COLOR, hasWarning ? [1, 0, 0, 1] : [0, 0, 0, 1], 1); |
|
warningText.graphics.foregroundColor = warningPen; |
|
warningText.window.update(); |
|
} catch (error) {} |
|
|
|
// 追加メッセージ(白・ボールド)を最下段に |
|
extraWarningText.text = extraWarningMessage ? ("\n" + extraWarningMessage) : " "; |
|
try { |
|
var extraWarningPen = extraWarningText.graphics.newPen(extraWarningText.graphics.PenType.SOLID_COLOR, [1, 1, 1, 1], 1); |
|
extraWarningText.graphics.foregroundColor = extraWarningPen; |
|
extraWarningText.window.update(); |
|
} catch (error) {} |
|
|
|
dialog.layout.layout(true); |
|
dialog.layout.resize(); |
|
} |
|
|
|
|
|
updateUI(); |
|
for (var i = 0; i < radioButtons.length; i++) { |
|
radioButtons[i].onClick = updateUI; |
|
} |
|
|
|
// 前回設定値を使用 チェックボックス(ボタン群の直上) |
|
var usePrevGroup = dialog.add('group'); |
|
usePrevGroup.alignment = ['left', 'center']; |
|
var usePrevCheckbox = usePrevGroup.add('checkbox', undefined, '前回設定値を使用'); |
|
usePrevCheckbox.value = customOptionsPrefs.usePrev === true; |
|
|
|
// ラジオボタンに値を設定する共通関数 |
|
function setRadioValues(radioIndex, upscaleMethod, downscaleMethod) { |
|
var validIndex = (radioIndex >= 0 && radioIndex < radioButtons.length) ? radioIndex : 0; |
|
for (var i = 0; i < radioButtons.length; i++) radioButtons[i].value = (i === validIndex); |
|
for (var j = 0; j < upscaleMethodRadioButtons.length; j++) upscaleMethodRadioButtons[j].value = (upscaleMethodValues[j] === upscaleMethod); |
|
for (var k = 0; k < downscaleMethodRadioButtons.length; k++) downscaleMethodRadioButtons[k].value = (downscaleMethodValues[k] === downscaleMethod); |
|
updateUI(); |
|
} |
|
|
|
function applyFromPrefs(prefs) { |
|
setRadioValues(prefs.radioIndex, prefs.upscaleMethod, prefs.downMethod); |
|
} |
|
|
|
function applyDefaultsNow() { |
|
setRadioValues(0, 'deepUpscale', 'bicubic'); |
|
} |
|
|
|
usePrevCheckbox.onClick = function() { |
|
if (usePrevCheckbox.value) applyFromPrefs(customOptionsPrefs); |
|
else applyDefaultsNow(); |
|
}; |
|
|
|
|
|
var buttonGroup = dialog.add("group"); |
|
buttonGroup.orientation = "row"; |
|
buttonGroup.alignment = "center"; // グループ自体を中央揃え |
|
buttonGroup.alignChildren = ["center", "center"]; // ボタンも中央揃え |
|
buttonGroup.margins = [0, 15, 0, 0]; |
|
var okButton = buttonGroup.add("button", undefined, "処理する (S / Enter)"); |
|
var cancelButton = buttonGroup.add("button", undefined, "キャンセル (Esc)"); |
|
var illustratorButton = buttonGroup.add("button", undefined, "Illustratorで選択 (I)"); |
|
var helpButton = buttonGroup.add("button", undefined, "?"); |
|
helpButton.preferredSize = [24, 24]; |
|
|
|
|
|
okButton.helpTip = "S キー または Enter で実行"; |
|
cancelButton.helpTip = "Esc でキャンセル"; |
|
illustratorButton.helpTip = "I キー で Illustrator で選択"; |
|
helpButton.helpTip = "このスクリプトの説明ページをブラウザで開きます"; |
|
|
|
|
|
okButton.properties = { |
|
name: 'ok' |
|
}; // Enter = OK |
|
cancelButton.properties = { |
|
name: 'cancel' |
|
}; // Esc = Cancel |
|
okButton.active = true; // 初期フォーカスをOKに |
|
dialog.defaultElement = okButton; // Enter で OK 実行 |
|
dialog.cancelElement = cancelButton; // Esc でキャンセル |
|
|
|
|
|
// ラジオボタンの選択値を取得する共通関数 |
|
function getSelectedRadioValues() { |
|
var selectedRadioIndex = 0; |
|
for (var i = 0; i < radioButtons.length; i++) { |
|
if (radioButtons[i].value) { |
|
selectedRadioIndex = i; |
|
break; |
|
} |
|
} |
|
var selectedUpscaleMethod = null; |
|
for (var j = 0; j < upscaleMethodRadioButtons.length; j++) { |
|
if (upscaleMethodRadioButtons[j].value) { |
|
selectedUpscaleMethod = upscaleMethodValues[j]; |
|
break; |
|
} |
|
} |
|
var selectedDownscaleMethod = null; |
|
for (var k = 0; k < downscaleMethodRadioButtons.length; k++) { |
|
if (downscaleMethodRadioButtons[k].value) { |
|
selectedDownscaleMethod = downscaleMethodValues[k]; |
|
break; |
|
} |
|
} |
|
return { |
|
radioIndex: selectedRadioIndex, |
|
upscaleMethod: selectedUpscaleMethod || 'deepUpscale', |
|
downMethod: selectedDownscaleMethod || 'bicubic' |
|
}; |
|
} |
|
|
|
// ダイアログクローズ時の共通処理 |
|
function savePrefsAndClose(exitCode, saveAll) { |
|
try { |
|
if (saveAll) { |
|
var selectedValues = getSelectedRadioValues(); |
|
savePrefsCO({ |
|
usePrev: usePrevCheckbox.value === true, |
|
radioIndex: selectedValues.radioIndex, |
|
upscaleMethod: selectedValues.upscaleMethod, |
|
downMethod: selectedValues.downMethod |
|
}); |
|
} else { |
|
saveUsePrevOnly(usePrevCheckbox.value); |
|
} |
|
} catch (error) {} |
|
try { |
|
dialog.close(exitCode); |
|
} catch (error) { |
|
dialog.close(); |
|
} |
|
} |
|
|
|
okButton.onClick = function() { |
|
savePrefsAndClose(1, true); |
|
}; |
|
|
|
cancelButton.onClick = function() { |
|
savePrefsAndClose(0, false); |
|
}; |
|
|
|
illustratorButton.onClick = function() { |
|
savePrefsAndClose(0, false); |
|
|
|
var selectionHandle = illustratorSelectionHandle || { |
|
itemId: null, |
|
placedIndex: null, |
|
pathFs: imagePath |
|
}; |
|
selectInIllustrator(selectionHandle); |
|
}; |
|
helpButton.onClick = function() { |
|
// 設定ダイアログは閉じずに、説明ページだけを開く |
|
openURLInBrowser("https://gist.github.com/Yamonov/b63d9c67402ef7af4c17ab33caccce31"); |
|
}; |
|
|
|
|
|
dialog.addEventListener('keydown', function(keyEvent) { |
|
try { |
|
var keyName = String(keyEvent.keyName || '').toUpperCase(); |
|
if (keyName === 'S') { |
|
okButton.notify('onClick'); |
|
keyEvent.preventDefault(); |
|
} else if (keyName === 'I') { |
|
illustratorButton.notify('onClick'); |
|
keyEvent.preventDefault(); |
|
} else if (keyName === 'ENTER' || keyName === 'RETURN') { |
|
okButton.notify('onClick'); |
|
keyEvent.preventDefault(); |
|
} |
|
} catch (error) {} |
|
}); |
|
|
|
|
|
dialog.onShow = function() { |
|
try { |
|
dialog.layout.layout(true); |
|
dialog.layout.resize(); |
|
dialog.center(); |
|
} catch (error) {} |
|
}; |
|
|
|
var isOk = (dialog.show() === 1); |
|
var result; |
|
if (isOk) { |
|
var selectedValues = getSelectedRadioValues(); |
|
var targetPpi = targetPPIList[selectedValues.radioIndex]; |
|
result = { |
|
ppi: targetPpi, |
|
method: selectedValues.upscaleMethod, |
|
downMethod: selectedValues.downMethod, |
|
usePrev: !!usePrevCheckbox.value, |
|
cancelled: false |
|
}; |
|
} else { |
|
result = { |
|
usePrev: !!usePrevCheckbox.value, |
|
cancelled: true |
|
}; |
|
} |
|
return result; |
|
} |
|
|
|
// ダイアログに表示する情報の整形(ファイル名・フォルダ・色情報→配置サイズ・ppi) |
|
// 画像情報収集(カラーモード/プロファイル/ファイル名/フォルダ) |
|
function modeToString(documentMode) { |
|
switch (documentMode) { |
|
case DocumentMode.BITMAP: |
|
return "Bitmap"; |
|
case DocumentMode.GRAYSCALE: |
|
return "Grayscale"; |
|
case DocumentMode.INDEXEDCOLOR: |
|
return "Indexed"; |
|
case DocumentMode.RGB: |
|
return "RGB"; |
|
case DocumentMode.CMYK: |
|
return "CMYK"; |
|
case DocumentMode.LAB: |
|
return "Lab"; |
|
case DocumentMode.MULTICHANNEL: |
|
return "Multichannel"; |
|
case DocumentMode.DUOTONE: |
|
return "Duotone"; |
|
default: |
|
return "Unknown"; |
|
} |
|
} |
|
var photoshopFileName = activeDoc.name; |
|
var photoshopFilePath; |
|
try { |
|
photoshopFilePath = activeDoc.fullName.parent.fsName; |
|
} catch (error) { |
|
photoshopFilePath = "(未保存)"; |
|
} // 表示はフォルダのみ(ファイル名は除外) |
|
var photoshopColorProfile; |
|
try { |
|
photoshopColorProfile = activeDoc.colorProfileName; |
|
} catch (error) { |
|
photoshopColorProfile = "(不明)"; |
|
} |
|
var photoshopColorMode = modeToString(activeDoc.mode); |
|
// Illustrator側選択用ハンドル(UI内 I ボタンで使用) |
|
var illustratorSelectionHandle = { |
|
itemId: illustratorItemId, |
|
placedIndex: illustratorPlacedIndex, |
|
pathFs: imagePath |
|
}; |
|
// ダイアログに表示する基本メッセージ(配置サイズとppi) |
|
var messageBase; |
|
if (placedItemsArray.length > 1) { |
|
messageBase = |
|
"ファイル名: " + photoshopFileName + "\n" + |
|
"フォルダ: " + photoshopFilePath + "\n" + |
|
"カラーモード:" + photoshopColorMode + "(" + photoshopColorProfile + ")\n" + |
|
"\n" + |
|
"処理対象の配置サイズ(長辺): " + longSideMM.toFixed(2) + " mm\n" + |
|
"処理対象の実効ppi: " + placedPPI.toFixed(2) + "\n" + |
|
"\n" + |
|
"配置点数: " + placedItemsArray.length + "\n" + |
|
"最小画像のppi: " + (isFinite(minSizePpi) ? minSizePpi.toFixed(2) : "-") + "\n" + |
|
"※最大サイズの画像を処理します。"; |
|
} else { |
|
messageBase = |
|
"ファイル名: " + photoshopFileName + "\n" + |
|
"フォルダ: " + photoshopFilePath + "\n" + |
|
"カラーモード:" + photoshopColorMode + "(" + photoshopColorProfile + ")\n" + |
|
"\n" + |
|
"処理対象の配置サイズ(長辺): " + longSideMM.toFixed(2) + " mm\n" + |
|
"処理対象の実効ppi: " + placedPPI.toFixed(2) + "\n"; |
|
} |
|
|
|
// 解像度選択ダイアログを表示し、ユーザーの選択を取得 |
|
var dialogResult = showConfirmDialog(messageBase, placedPPI, imagePath); |
|
// ダイアログクローズ時にも usePrev を保存 |
|
try { |
|
if (dialogResult && dialogResult.hasOwnProperty('usePrev')) saveUsePrevOnly(dialogResult.usePrev); |
|
} catch (error) {} |
|
|
|
// キャンセル・クローズ時は処理を中止 |
|
if (!dialogResult || dialogResult.cancelled) { |
|
return; |
|
} |
|
|
|
var targetPPI = dialogResult.ppi; |
|
var upscaleMethod = dialogResult.method; |
|
var downscaleMethod = dialogResult.downMethod; |
|
var scaleRatio = targetPPI / placedPPI; |
|
|
|
// 長辺を拡縮率に応じてリサイズ(縦横比維持) |
|
var newWidthPixels, newHeightPixels; |
|
if (documentWidthPixels >= documentHeightPixels) { |
|
newWidthPixels = longSidePixels * scaleRatio; |
|
newHeightPixels = documentHeightPixels * scaleRatio; |
|
} else { |
|
newHeightPixels = longSidePixels * scaleRatio; |
|
newWidthPixels = documentWidthPixels * scaleRatio; |
|
} |
|
|
|
// 縮小/拡大でメソッドを分岐 |
|
if (scaleRatio < 1) { |
|
if (downscaleMethod === "bicubic") { |
|
activeDoc.resizeImage(UnitValue(newWidthPixels, "px"), UnitValue(newHeightPixels, "px"), targetPPI, ResampleMethod.BICUBIC); |
|
} else if (downscaleMethod === "nearestNeighbor") { |
|
activeDoc.resizeImage(UnitValue(newWidthPixels, "px"), UnitValue(newHeightPixels, "px"), targetPPI, ResampleMethod.NEARESTNEIGHBOR); |
|
} else { |
|
// 万一未知の値の場合はBICUBIC |
|
activeDoc.resizeImage(UnitValue(newWidthPixels, "px"), UnitValue(newHeightPixels, "px"), targetPPI, ResampleMethod.BICUBIC); |
|
} |
|
} else { |
|
// 拡大時は選択された拡大メソッドで分岐 |
|
if (upscaleMethod === "deepUpscale") { |
|
// ActionDescriptorでディテールを保持2.0(推奨) |
|
var resizeDescriptor = new ActionDescriptor(); |
|
resizeDescriptor.putUnitDouble(charIDToTypeID('Wdth'), charIDToTypeID('#Pxl'), newWidthPixels); |
|
resizeDescriptor.putUnitDouble(charIDToTypeID('Hght'), charIDToTypeID('#Pxl'), newHeightPixels); |
|
resizeDescriptor.putUnitDouble(charIDToTypeID('Rslt'), charIDToTypeID('#Rsl'), targetPPI); |
|
resizeDescriptor.putBoolean(stringIDToTypeID('scaleStyles'), true); |
|
resizeDescriptor.putEnumerated(charIDToTypeID('Intr'), charIDToTypeID('Intp'), stringIDToTypeID('deepUpscale')); |
|
executeAction(charIDToTypeID('ImgS'), resizeDescriptor, DialogModes.NO); |
|
} else if (upscaleMethod === "preserveDetails") { |
|
// ResampleMethod.PRESERVEDETAILS |
|
activeDoc.resizeImage(UnitValue(newWidthPixels, "px"), UnitValue(newHeightPixels, "px"), targetPPI, ResampleMethod.PRESERVEDETAILS); |
|
} else if (upscaleMethod === "nearestNeighbor") { |
|
// ResampleMethod.NEARESTNEIGHBOR |
|
activeDoc.resizeImage(UnitValue(newWidthPixels, "px"), UnitValue(newHeightPixels, "px"), targetPPI, ResampleMethod.NEARESTNEIGHBOR); |
|
} else { |
|
// 万一未知の値の場合はBICUBIC |
|
activeDoc.resizeImage(UnitValue(newWidthPixels, "px"), UnitValue(newHeightPixels, "px"), targetPPI, ResampleMethod.BICUBIC); |
|
} |
|
} |
|
// 最後に100%表示に切り替え |
|
app.runMenuItem(stringIDToTypeID("actualPixels")); |
|
}; |
|
|
|
// ----- BridgeTalk通信エラー(Illustrator未起動/応答なし等) |
|
bridgeTalk.onError = function(errorEvent) { |
|
alert("Illustratorとの通信エラー: " + errorEvent.body); |
|
}; |
|
|
|
try { |
|
bridgeTalk.timeout = 30; |
|
} catch (error) {} |
|
bridgeTalk.send(); |
|
|
|
} |
|
|
|
// ===== Illustrator側:対象パスの配置アイテムを探索し一致した全配置の長辺(pt)等を返す ===== |
|
function illustratorSideForPT(photoshopImagePath) { |
|
var targetPath = photoshopImagePath; |
|
var isWindows = $.os.indexOf("Windows") >= 0; |
|
|
|
function canonFsAlready(filePath) { |
|
if (!filePath) return filePath; |
|
var normalizedPath = String(filePath); |
|
if (isWindows) { |
|
// Remove "\\?\" prefix and normalize "\\?\UNC\server\share" -> "\\server\share" |
|
if (normalizedPath.length >= 4 && normalizedPath.substring(0, 4) === "\\\\?\\") { |
|
var restOfPath = normalizedPath.substring(4); |
|
if (restOfPath.length >= 4 && restOfPath.substring(0, 4).toLowerCase() === "unc\\") { |
|
normalizedPath = "\\\\" + restOfPath.substring(4); // prepend UNC prefix |
|
} else { |
|
normalizedPath = restOfPath; // long-path local form: \\?\C:\ -> C:\ |
|
} |
|
} |
|
// Normalize separators to backslash |
|
normalizedPath = normalizedPath.split('/').join('\\'); |
|
// Collapse redundant backslashes (except keep leading UNC double backslash) |
|
normalizedPath = normalizedPath.replace(/\\+/g, "\\"); |
|
// Unicode正規化 (NFC) を File() 経由で実現 |
|
try { |
|
normalizedPath = File(normalizedPath).fsName; // 実在しないパスでもOK、NFC寄せと区切り統一がかかる |
|
} catch (error) {} |
|
} |
|
return normalizedPath; |
|
} |
|
|
|
var canonicalTargetPath = canonFsAlready(targetPath); |
|
var rawTargetPath = targetPath; // Windows用にまずは生の比較を試す |
|
if (app.documents.length === 0) return null; |
|
|
|
var doc = app.activeDocument; |
|
var placedItems = doc.placedItems; |
|
var matchedItems = []; |
|
|
|
// basename抽出(ループ外で事前計算) |
|
var targetBaseName = ""; |
|
try { |
|
var targetPathString = String(isWindows ? rawTargetPath : canonicalTargetPath); |
|
var lastSlash = Math.max(targetPathString.lastIndexOf('/'), targetPathString.lastIndexOf('\\')); |
|
targetBaseName = (lastSlash >= 0) ? targetPathString.substring(lastSlash + 1) : targetPathString; |
|
targetBaseName = targetBaseName.toLowerCase(); |
|
} catch (error) {} |
|
|
|
// 回転・斜行に依存しない長辺(pt)を算出(bboxは最初の一致のみ取得し、以降は行列スケールだけを適用) |
|
var baseWidthPt = null; |
|
var baseHeightPt = null; |
|
for (var i = 0; i < placedItems.length; i++) { |
|
var item = placedItems[i]; |
|
var isMatched = false; |
|
|
|
// 1) 最軽量: basename一致チェック(小文字化のみ) |
|
try { |
|
var itemName = String(item.name || "").toLowerCase(); |
|
if (itemName && targetBaseName && itemName === targetBaseName) { |
|
isMatched = true; |
|
} |
|
} catch (error) {} |
|
|
|
if (!isMatched) { |
|
// 2) 軽量: fsNameによるRAW文字列比較 |
|
var itemFilePath = null; |
|
try { |
|
if (item.file && item.file.fsName) itemFilePath = item.file.fsName; |
|
} catch (error) { |
|
itemFilePath = null; |
|
} |
|
|
|
if (itemFilePath) { |
|
if (isWindows) { |
|
var rawItemPath = String(itemFilePath); |
|
var rawTarget = String(rawTargetPath); |
|
if (rawItemPath === rawTarget || rawItemPath.toLowerCase() === rawTarget.toLowerCase()) { |
|
isMatched = true; |
|
} else { |
|
// 3) 重量: File().fsNameを含む正規化比較(最後の手段) |
|
var canonicalItemPath = canonFsAlready(rawItemPath); |
|
var canonicalTarget = canonicalTargetPath; |
|
if (canonicalItemPath === canonicalTarget || canonicalItemPath.toLowerCase() === canonicalTarget.toLowerCase()) { |
|
isMatched = true; |
|
} |
|
} |
|
} else { |
|
// mac等: canonical比較 |
|
var canonicalItemPath = canonFsAlready(itemFilePath); |
|
if (canonicalItemPath === canonicalTargetPath) { |
|
isMatched = true; |
|
} |
|
} |
|
} |
|
} |
|
|
|
if (!isMatched) continue; |
|
|
|
if (baseWidthPt === null || baseHeightPt === null) { |
|
// 最初のヒットでのみ未変形サイズ(pt)を取得 |
|
try { |
|
var boundingBox = item.boundingBox; // [y1,x1,y2,x2] |
|
baseWidthPt = Math.abs(boundingBox[3] - boundingBox[1]); |
|
baseHeightPt = Math.abs(boundingBox[0] - boundingBox[2]); |
|
} catch (error) { |
|
baseWidthPt = 0; |
|
baseHeightPt = 0; |
|
} |
|
} |
|
var longSidePt = 0; |
|
try { |
|
var matrix = item.matrix; |
|
var scaleX = Math.sqrt(matrix.mValueA * matrix.mValueA + matrix.mValueB * matrix.mValueB); |
|
var scaleY = Math.sqrt(matrix.mValueC * matrix.mValueC + matrix.mValueD * matrix.mValueD); |
|
var widthPt = baseWidthPt * scaleX; |
|
var heightPt = baseHeightPt * scaleY; |
|
if (widthPt > 0 && heightPt > 0) longSidePt = Math.max(widthPt, heightPt); |
|
} catch (error) { |
|
longSidePt = 0; |
|
} |
|
var linkStatus = 0; |
|
try { |
|
if (typeof item.linkStatus !== 'undefined') linkStatus = item.linkStatus; |
|
} catch (error) { |
|
linkStatus = 0; |
|
} |
|
var placedItemId = null; |
|
try { |
|
placedItemId = item.id; |
|
} catch (error) { |
|
placedItemId = null; |
|
} |
|
matchedItems.push({ |
|
longSidePt: longSidePt, |
|
linkStatus: linkStatus, |
|
itemId: placedItemId, |
|
placedIndex: i |
|
}); |
|
} |
|
|
|
if (!matchedItems.length) return "null"; |
|
// JSON.stringify が無い環境向け:オブジェクトリテラル文字列を手組みで返す |
|
var jsonParts = []; |
|
for (var resultIndex = 0; resultIndex < matchedItems.length; resultIndex++) { |
|
var resultItem = matchedItems[resultIndex]; |
|
var jsonSegment = "{longSidePt:" + (resultItem.longSidePt || 0) + ", linkStatus:" + (resultItem.linkStatus || 0) + ", itemId:" + (resultItem.itemId != null ? resultItem.itemId : "null") + ", placedIndex:" + (resultItem.placedIndex != null ? resultItem.placedIndex : "null") + "}"; |
|
jsonParts.push(jsonSegment); |
|
} |
|
return "{items:[" + jsonParts.join(",") + "]}"; |
|
} |
|
|
|
// ===== 実行開始 ===== |
|
main(); |