Created
August 8, 2025 09:10
-
-
Save monokano/fbe7e17d870f710f52f9cd1b8d523688 to your computer and use it in GitHub Desktop.
選択オブジェクトを個別に拡大縮小して間隔を維持するInDesignスクリプト
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Adobe InDesign ExtendScript - 選択オブジェクト拡大縮小 | |
| (function() { | |
| // メイン関数 | |
| function main() { | |
| // ドキュメントが開いているかチェック | |
| if (app.documents.length == 0) { | |
| alert("ドキュメントが開かれていません。"); | |
| return; | |
| } | |
| // 選択オブジェクトをチェック | |
| if (app.selection.length == 0) { | |
| alert("オブジェクトが選択されていません。"); | |
| return; | |
| } | |
| // ダイアログを表示 | |
| var dialog = createDialog(); | |
| var result = dialog.show(); | |
| if (result == 1) { // OKボタンが押された場合 | |
| var horizontalPercent = parseFloat(dialog.scaleGroup.horizontalInput.text); | |
| var verticalPercent = parseFloat(dialog.scaleGroup.verticalInput.text); | |
| var anchorPoint = dialog.anchorGroup.anchorDropdown.selection.index; | |
| if (isNaN(horizontalPercent) || horizontalPercent <= 0 || | |
| isNaN(verticalPercent) || verticalPercent <= 0) { | |
| alert("有効な数値を入力してください。"); | |
| return; | |
| } | |
| // 100%/100%の場合は処理をスキップ | |
| if (horizontalPercent == 100 && verticalPercent == 100) { | |
| alert("100%/100%では変更されません。"); | |
| return; | |
| } | |
| // Undo履歴を残さずに実行 | |
| app.doScript(function() { | |
| scaleSelectedObjects(horizontalPercent, verticalPercent, anchorPoint); | |
| }, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT); | |
| } | |
| } | |
| // 選択オブジェクトを展開(グループを含む) | |
| function expandSelection(selection) { | |
| var expandedObjects = []; | |
| for (var i = 0; i < selection.length; i++) { | |
| var obj = selection[i]; | |
| expandObjectsRecursively(obj, expandedObjects); | |
| } | |
| return expandedObjects; | |
| } | |
| // オブジェクトを再帰的に展開 | |
| function expandObjectsRecursively(obj, result) { | |
| // グループの場合 | |
| if (obj.constructor.name == "Group") { | |
| // グループ内のオブジェクトを再帰的に展開 | |
| for (var i = 0; i < obj.allPageItems.length; i++) { | |
| expandObjectsRecursively(obj.allPageItems[i], result); | |
| } | |
| } else { | |
| // グループではない場合、結果に追加 | |
| result.push(obj); | |
| } | |
| } | |
| // ダイアログUI作成 | |
| function createDialog() { | |
| var dialog = new Window("dialog", "オブジェクト拡大縮小"); | |
| dialog.orientation = "column"; | |
| dialog.alignChildren = "fill"; | |
| dialog.spacing = 10; | |
| dialog.margins = 16; | |
| // 拡大縮小率入力 | |
| dialog.scaleGroup = dialog.add("group"); | |
| dialog.scaleGroup.orientation = "column"; | |
| dialog.scaleGroup.alignChildren = "fill"; | |
| // 横幅 | |
| dialog.scaleGroup.horizontalGroup = dialog.scaleGroup.add("group"); | |
| dialog.scaleGroup.horizontalGroup.orientation = "row"; | |
| dialog.scaleGroup.horizontalGroup.alignChildren = "center"; | |
| dialog.scaleGroup.horizontalGroup.add("statictext", undefined, "横幅:"); | |
| dialog.scaleGroup.horizontalInput = dialog.scaleGroup.horizontalGroup.add("edittext", undefined, "100"); | |
| dialog.scaleGroup.horizontalInput.characters = 8; | |
| dialog.scaleGroup.horizontalGroup.add("statictext", undefined, "%"); | |
| // 縦幅 | |
| dialog.scaleGroup.verticalGroup = dialog.scaleGroup.add("group"); | |
| dialog.scaleGroup.verticalGroup.orientation = "row"; | |
| dialog.scaleGroup.verticalGroup.alignChildren = "center"; | |
| dialog.scaleGroup.verticalGroup.add("statictext", undefined, "縦幅:"); | |
| dialog.scaleGroup.verticalInput = dialog.scaleGroup.verticalGroup.add("edittext", undefined, "100"); | |
| dialog.scaleGroup.verticalInput.characters = 8; | |
| dialog.scaleGroup.verticalGroup.add("statictext", undefined, "%"); | |
| // 比率固定チェックボックス | |
| dialog.scaleGroup.lockGroup = dialog.scaleGroup.add("group"); | |
| dialog.scaleGroup.lockGroup.orientation = "row"; | |
| dialog.scaleGroup.lockGroup.alignChildren = "left"; | |
| dialog.scaleGroup.lockRatio = dialog.scaleGroup.lockGroup.add("checkbox", undefined, "縦横比を固定"); | |
| dialog.scaleGroup.lockRatio.value = true; // デフォルトでON | |
| // 基準点選択 | |
| dialog.anchorGroup = dialog.add("group"); | |
| dialog.anchorGroup.orientation = "row"; | |
| dialog.anchorGroup.alignChildren = "center"; | |
| dialog.anchorGroup.add("statictext", undefined, "基準点:"); | |
| dialog.anchorGroup.anchorDropdown = dialog.anchorGroup.add("dropdownlist", undefined, [ | |
| "左上", "上中央", "右上", | |
| "左中央", "中央", "右中央", | |
| "左下", "下中央", "右下" | |
| ]); | |
| dialog.anchorGroup.anchorDropdown.selection = 4; // デフォルトは中央 | |
| // 比率固定の動作 | |
| dialog.scaleGroup.horizontalInput.onChanging = function() { | |
| if (dialog.scaleGroup.lockRatio.value) { | |
| dialog.scaleGroup.verticalInput.text = dialog.scaleGroup.horizontalInput.text; | |
| } | |
| updateButtonState(); | |
| }; | |
| dialog.scaleGroup.verticalInput.onChanging = function() { | |
| if (dialog.scaleGroup.lockRatio.value) { | |
| dialog.scaleGroup.horizontalInput.text = dialog.scaleGroup.verticalInput.text; | |
| } | |
| updateButtonState(); | |
| }; | |
| // 縦横比固定チェックボックスの動作 | |
| dialog.scaleGroup.lockRatio.onClick = function() { | |
| if (dialog.scaleGroup.lockRatio.value) { | |
| // チェックをONにした時:横の値に縦を合わせる | |
| dialog.scaleGroup.verticalInput.text = dialog.scaleGroup.horizontalInput.text; | |
| } | |
| updateButtonState(); | |
| }; | |
| // ボタンの有効/無効を制御 | |
| function updateButtonState() { | |
| var horizontalValue = parseFloat(dialog.scaleGroup.horizontalInput.text); | |
| var verticalValue = parseFloat(dialog.scaleGroup.verticalInput.text); | |
| // 横100かつ縦100の場合はボタンを無効にする | |
| if (horizontalValue == 100 && verticalValue == 100) { | |
| buttonGroup.okButton.enabled = false; | |
| } else { | |
| buttonGroup.okButton.enabled = true; | |
| } | |
| } | |
| // 説明テキスト | |
| var infoPanel = dialog.add("panel", undefined, "処理内容"); | |
| infoPanel.orientation = "column"; | |
| infoPanel.alignChildren = "fill"; | |
| infoPanel.add("statictext", undefined, "• グラフィックフレーム: 縦横それぞれ拡大縮小"); | |
| infoPanel.add("statictext", undefined, "• テキストフレーム: 横幅のみ拡大縮小(縦幅は無視)"); | |
| infoPanel.add("statictext", undefined, "• オブジェクトの相対位置関係は維持されます"); | |
| infoPanel.add("statictext", undefined, "• グループ化されたオブジェクトにも対応"); | |
| infoPanel.add("statictext", undefined, "• 横100%かつ縦100%の場合は実行できません"); | |
| // ボタン | |
| var buttonGroup = dialog.add("group"); | |
| buttonGroup.orientation = "row"; | |
| buttonGroup.alignment = "center"; | |
| buttonGroup.add("button", undefined, "キャンセル", {name: "cancel"}); | |
| buttonGroup.add("button", undefined, "実行", {name: "ok"}); | |
| return dialog; | |
| } | |
| // 基準点のAnchorPointを取得 | |
| function getAnchorPoint(index) { | |
| var anchorPoints = [ | |
| AnchorPoint.TOP_LEFT_ANCHOR, | |
| AnchorPoint.TOP_CENTER_ANCHOR, | |
| AnchorPoint.TOP_RIGHT_ANCHOR, | |
| AnchorPoint.LEFT_CENTER_ANCHOR, | |
| AnchorPoint.CENTER_ANCHOR, | |
| AnchorPoint.RIGHT_CENTER_ANCHOR, | |
| AnchorPoint.BOTTOM_LEFT_ANCHOR, | |
| AnchorPoint.BOTTOM_CENTER_ANCHOR, | |
| AnchorPoint.BOTTOM_RIGHT_ANCHOR | |
| ]; | |
| return anchorPoints[index]; | |
| } | |
| // 選択オブジェクトを拡大縮小(間隔維持) | |
| function scaleSelectedObjects(horizontalPercent, verticalPercent, anchorIndex) { | |
| var doc = app.activeDocument; | |
| var horizontalRatio = horizontalPercent / 100; | |
| var verticalRatio = verticalPercent / 100; | |
| // 選択オブジェクトを展開(グループを含む) | |
| var allObjects = expandSelection(app.selection); | |
| // 元の全体境界を取得 | |
| var originalBounds = getSelectionBounds(allObjects); | |
| // 基準点の座標を計算 | |
| var anchorX = calculateAnchorX(originalBounds, anchorIndex); | |
| var anchorY = calculateAnchorY(originalBounds, anchorIndex); | |
| // 各オブジェクトの元情報を記録 | |
| var objectData = []; | |
| for (var i = 0; i < allObjects.length; i++) { | |
| var obj = allObjects[i]; | |
| var bounds = obj.geometricBounds; | |
| objectData.push({ | |
| object: obj, | |
| originalBounds: [bounds[0], bounds[1], bounds[2], bounds[3]], | |
| originalWidth: bounds[3] - bounds[1], | |
| originalHeight: bounds[2] - bounds[0], | |
| isGraphic: isGraphicFrame(obj), | |
| isText: isTextFrame(obj) | |
| }); | |
| } | |
| // Y座標でグループ化(行の検出) | |
| var rows = groupByRows(objectData); | |
| // X座標でグループ化(列の検出) | |
| var columns = groupByColumns(objectData); | |
| // 行間と列間の間隔を計算 | |
| var rowGaps = calculateRowGaps(rows); | |
| var columnGaps = calculateColumnGaps(columns); | |
| // 各オブジェクトのサイズを変更 | |
| for (var i = 0; i < objectData.length; i++) { | |
| var data = objectData[i]; | |
| var obj = data.object; | |
| var newWidth, newHeight; | |
| if (data.isText) { | |
| // テキストフレーム:横幅のみ拡大縮小 | |
| newWidth = data.originalWidth * horizontalRatio; | |
| newHeight = data.originalHeight; // 高さは変更しない | |
| } else { | |
| // グラフィックフレームやその他:縦横それぞれ拡大縮小 | |
| newWidth = data.originalWidth * horizontalRatio; | |
| newHeight = data.originalHeight * verticalRatio; | |
| } | |
| // 左上を基準にサイズ変更 | |
| var bounds = data.originalBounds; | |
| obj.geometricBounds = [ | |
| bounds[0], // top (変更なし) | |
| bounds[1], // left (変更なし) | |
| bounds[0] + newHeight, // bottom | |
| bounds[1] + newWidth // right | |
| ]; | |
| // 内容の拡大縮小(グラフィックフレームの場合) | |
| if (data.isGraphic && obj.allGraphics.length > 0) { | |
| obj.allGraphics[0].horizontalScale *= horizontalRatio; | |
| if (!data.isText) { | |
| obj.allGraphics[0].verticalScale *= verticalRatio; | |
| } | |
| } | |
| } | |
| // 列間隔を調整 | |
| adjustColumnSpacing(columns, columnGaps); | |
| // 行間隔を調整 | |
| adjustRowSpacing(rows, rowGaps); | |
| // 基準点に合わせて全体を移動 | |
| var newBounds = getSelectionBounds(allObjects); | |
| var newAnchorX = calculateAnchorX(newBounds, anchorIndex); | |
| var newAnchorY = calculateAnchorY(newBounds, anchorIndex); | |
| var offsetX = anchorX - newAnchorX; | |
| var offsetY = anchorY - newAnchorY; | |
| // 全オブジェクトを移動 | |
| for (var i = 0; i < allObjects.length; i++) { | |
| var obj = allObjects[i]; | |
| var bounds = obj.geometricBounds; | |
| obj.geometricBounds = [ | |
| bounds[0] + offsetY, | |
| bounds[1] + offsetX, | |
| bounds[2] + offsetY, | |
| bounds[3] + offsetX | |
| ]; | |
| } | |
| } | |
| // Y座標で行にグループ化 | |
| function groupByRows(objectData) { | |
| var rows = []; | |
| var tolerance = 1; // 1ポイントの誤差を許容 | |
| for (var i = 0; i < objectData.length; i++) { | |
| var obj = objectData[i]; | |
| var top = obj.originalBounds[0]; | |
| // 既存の行を検索 | |
| var foundRow = false; | |
| for (var r = 0; r < rows.length; r++) { | |
| var rowTop = rows[r][0].originalBounds[0]; | |
| if (Math.abs(top - rowTop) <= tolerance) { | |
| rows[r].push(obj); | |
| foundRow = true; | |
| break; | |
| } | |
| } | |
| // 新しい行を作成 | |
| if (!foundRow) { | |
| rows.push([obj]); | |
| } | |
| } | |
| // 各行を上から下の順にソート | |
| rows.sort(function(a, b) { | |
| return a[0].originalBounds[0] - b[0].originalBounds[0]; | |
| }); | |
| // 各行内を左から右の順にソート | |
| for (var r = 0; r < rows.length; r++) { | |
| rows[r].sort(function(a, b) { | |
| return a.originalBounds[1] - b.originalBounds[1]; | |
| }); | |
| } | |
| return rows; | |
| } | |
| // X座標で列にグループ化 | |
| function groupByColumns(objectData) { | |
| var columns = []; | |
| var tolerance = 1; // 1ポイントの誤差を許容 | |
| for (var i = 0; i < objectData.length; i++) { | |
| var obj = objectData[i]; | |
| var left = obj.originalBounds[1]; | |
| // 既存の列を検索 | |
| var foundColumn = false; | |
| for (var c = 0; c < columns.length; c++) { | |
| var columnLeft = columns[c][0].originalBounds[1]; | |
| if (Math.abs(left - columnLeft) <= tolerance) { | |
| columns[c].push(obj); | |
| foundColumn = true; | |
| break; | |
| } | |
| } | |
| // 新しい列を作成 | |
| if (!foundColumn) { | |
| columns.push([obj]); | |
| } | |
| } | |
| // 各列を左から右の順にソート | |
| columns.sort(function(a, b) { | |
| return a[0].originalBounds[1] - b[0].originalBounds[1]; | |
| }); | |
| // 各列内を上から下の順にソート | |
| for (var c = 0; c < columns.length; c++) { | |
| columns[c].sort(function(a, b) { | |
| return a.originalBounds[0] - b.originalBounds[0]; | |
| }); | |
| } | |
| return columns; | |
| } | |
| // 行間の間隔を計算 | |
| function calculateRowGaps(rows) { | |
| var gaps = []; | |
| for (var i = 0; i < rows.length - 1; i++) { | |
| // 現在の行の最大bottom | |
| var currentBottom = rows[i][0].originalBounds[2]; | |
| for (var j = 1; j < rows[i].length; j++) { | |
| if (rows[i][j].originalBounds[2] > currentBottom) { | |
| currentBottom = rows[i][j].originalBounds[2]; | |
| } | |
| } | |
| // 次の行の最小top | |
| var nextTop = rows[i + 1][0].originalBounds[0]; | |
| for (var j = 1; j < rows[i + 1].length; j++) { | |
| if (rows[i + 1][j].originalBounds[0] < nextTop) { | |
| nextTop = rows[i + 1][j].originalBounds[0]; | |
| } | |
| } | |
| gaps.push(nextTop - currentBottom); | |
| } | |
| return gaps; | |
| } | |
| // 列間の間隔を計算 | |
| function calculateColumnGaps(columns) { | |
| var gaps = []; | |
| for (var i = 0; i < columns.length - 1; i++) { | |
| // 現在の列の最大right | |
| var currentRight = columns[i][0].originalBounds[3]; | |
| for (var j = 1; j < columns[i].length; j++) { | |
| if (columns[i][j].originalBounds[3] > currentRight) { | |
| currentRight = columns[i][j].originalBounds[3]; | |
| } | |
| } | |
| // 次の列の最小left | |
| var nextLeft = columns[i + 1][0].originalBounds[1]; | |
| for (var j = 1; j < columns[i + 1].length; j++) { | |
| if (columns[i + 1][j].originalBounds[1] < nextLeft) { | |
| nextLeft = columns[i + 1][j].originalBounds[1]; | |
| } | |
| } | |
| gaps.push(nextLeft - currentRight); | |
| } | |
| return gaps; | |
| } | |
| // 列間隔を調整 | |
| function adjustColumnSpacing(columns, columnGaps) { | |
| for (var c = 1; c < columns.length; c++) { | |
| // 前の列の現在の最大right | |
| var prevColumnRight = columns[c - 1][0].object.geometricBounds[3]; | |
| for (var j = 1; j < columns[c - 1].length; j++) { | |
| if (columns[c - 1][j].object.geometricBounds[3] > prevColumnRight) { | |
| prevColumnRight = columns[c - 1][j].object.geometricBounds[3]; | |
| } | |
| } | |
| // 目標left位置 | |
| var targetLeft = prevColumnRight + columnGaps[c - 1]; | |
| // 現在の列の現在のleft | |
| var currentLeft = columns[c][0].object.geometricBounds[1]; | |
| for (var j = 1; j < columns[c].length; j++) { | |
| if (columns[c][j].object.geometricBounds[1] < currentLeft) { | |
| currentLeft = columns[c][j].object.geometricBounds[1]; | |
| } | |
| } | |
| // 移動量 | |
| var offsetX = targetLeft - currentLeft; | |
| // 列内の全オブジェクトを移動 | |
| for (var i = 0; i < columns[c].length; i++) { | |
| var obj = columns[c][i].object; | |
| var bounds = obj.geometricBounds; | |
| obj.geometricBounds = [ | |
| bounds[0], | |
| bounds[1] + offsetX, | |
| bounds[2], | |
| bounds[3] + offsetX | |
| ]; | |
| } | |
| } | |
| } | |
| // 行間隔を調整 | |
| function adjustRowSpacing(rows, rowGaps) { | |
| for (var r = 1; r < rows.length; r++) { | |
| // 前の行の現在の最大bottom | |
| var prevRowBottom = rows[r - 1][0].object.geometricBounds[2]; | |
| for (var j = 1; j < rows[r - 1].length; j++) { | |
| if (rows[r - 1][j].object.geometricBounds[2] > prevRowBottom) { | |
| prevRowBottom = rows[r - 1][j].object.geometricBounds[2]; | |
| } | |
| } | |
| // 目標top位置 | |
| var targetTop = prevRowBottom + rowGaps[r - 1]; | |
| // 現在の行の現在のtop | |
| var currentTop = rows[r][0].object.geometricBounds[0]; | |
| for (var j = 1; j < rows[r].length; j++) { | |
| if (rows[r][j].object.geometricBounds[0] < currentTop) { | |
| currentTop = rows[r][j].object.geometricBounds[0]; | |
| } | |
| } | |
| // 移動量 | |
| var offsetY = targetTop - currentTop; | |
| // 行内の全オブジェクトを移動 | |
| for (var i = 0; i < rows[r].length; i++) { | |
| var obj = rows[r][i].object; | |
| var bounds = obj.geometricBounds; | |
| obj.geometricBounds = [ | |
| bounds[0] + offsetY, | |
| bounds[1], | |
| bounds[2] + offsetY, | |
| bounds[3] | |
| ]; | |
| } | |
| } | |
| } | |
| // 基準点のX座標を計算 | |
| function calculateAnchorX(bounds, anchorIndex) { | |
| var width = bounds[3] - bounds[1]; | |
| switch (anchorIndex) { | |
| case 0: case 3: case 6: // 左 | |
| return bounds[1]; | |
| case 1: case 4: case 7: // 中央 | |
| return bounds[1] + width / 2; | |
| case 2: case 5: case 8: // 右 | |
| return bounds[3]; | |
| } | |
| } | |
| // 基準点のY座標を計算 | |
| function calculateAnchorY(bounds, anchorIndex) { | |
| var height = bounds[2] - bounds[0]; | |
| switch (anchorIndex) { | |
| case 0: case 1: case 2: // 上 | |
| return bounds[0]; | |
| case 3: case 4: case 5: // 中央 | |
| return bounds[0] + height / 2; | |
| case 6: case 7: case 8: // 下 | |
| return bounds[2]; | |
| } | |
| } | |
| // 選択範囲全体の境界を取得 | |
| function getSelectionBounds(objects) { | |
| var minTop = Number.MAX_VALUE; | |
| var minLeft = Number.MAX_VALUE; | |
| var maxBottom = -Number.MAX_VALUE; | |
| var maxRight = -Number.MAX_VALUE; | |
| for (var i = 0; i < objects.length; i++) { | |
| var bounds = objects[i].geometricBounds; | |
| if (bounds[0] < minTop) minTop = bounds[0]; | |
| if (bounds[1] < minLeft) minLeft = bounds[1]; | |
| if (bounds[2] > maxBottom) maxBottom = bounds[2]; | |
| if (bounds[3] > maxRight) maxRight = bounds[3]; | |
| } | |
| return [minTop, minLeft, maxBottom, maxRight]; | |
| } | |
| // グラフィックフレーム判定 | |
| function isGraphicFrame(obj) { | |
| return (obj.constructor.name == "Rectangle" || | |
| obj.constructor.name == "Oval" || | |
| obj.constructor.name == "Polygon") && | |
| obj.contentType == ContentType.GRAPHIC_TYPE; | |
| } | |
| // テキストフレーム判定 | |
| function isTextFrame(obj) { | |
| return obj.constructor.name == "TextFrame" || | |
| (obj.constructor.name == "Rectangle" && obj.contentType == ContentType.TEXT_TYPE); | |
| } | |
| // メイン実行 | |
| try { | |
| main(); | |
| } catch (e) { | |
| alert("エラーが発生しました: " + e.message); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment