【解決】Illustratorでもテキストを行ごとに分割したい!
したたか企画では,テキストを行ごとに分割する機能をAdobe XD用・Photoshop用にそれぞれ公開しています。しかしIllustrator用はまだでした。というのも,テキストばらしやテキストばらしAIなど実用的な定番スクリプトがすでにあるからです。
ただもうXDとPhotoshop用とFigma用に3つも提供しましたし,せっかくなのでIllustrator用も公開するしかないでしょう。それが今回紹介する 複数行のテキストを1行ごとに分割するIllustrator用スクリプト,Split Rows for Illustrator.jsx です。※都合によりSplit Rows for Ai.jsxから名前を変更しました。
既存スクリプトでも特に困っていないと思いますが,Split Rows for Illustrator.jsxのいいところは次の通りです。ぜひお試しください。
- もとのフォントやサイズ,色などを保持
- 横書き・縦書き両方に対応
- 自動行送りや段落前後のアキありでも見た目をキープ
- エリア内文字にも対応
- 選択したのがグループでも,配下のテキストを自動取得して実行
- テキストの中身を編集状態のときは,その親のテキストフレームに対して実行
Split Rows for Illustrator.jsxって何?
Illustrator用スクリプト(JavaScript)です。複数行のテキストを選択して実行すると1行ごとに分割されます。v2.0.0からは,エリア内文字(エリアテキスト・エリア内テキスト)が段落ごとに分かれるようになりました。
macOS/Windows問わず使用可能です。Adobe Illustrator CS6かそれ以降を対象としていますが,おそらくそれ以前でも動きます。
動作確認済み- macOS 10.14(Intel),11.6/12.7/14.3(Apple Silicon)
- Windows 10
- Illustrator CS6(v16),2019(v23)〜2024(v28)
こちらからダウンロードしてください。
Split Rows for Illustrator.jsx 1 ファイル 2.91 KB ダウンロードより進化した後継ツールがあります。お求めのかたは【解決】Illustratorでテキストを分割・結合したい!へどうぞ。
使いかたは?
テキストを選択してスクリプトを実行するだけです。
これでまた少し仕事が速くなりました。今日もさっさと仕事を切り上げて好きなことをしましょう!
作者に感謝を伝えたい!
Buy me a coffeeは,クレジットカード払いなどでクリエイターにコーヒーをおごれるサービスです。スクリプトが役に立った! 感謝の気持ちを表現したい! というかた,おごっていただけましたら嬉しいです☕️
コードはこちら。
Split Rows for Ai.jsx JavaScript 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537 /** * @file テキストを行ごとに分割する * @version 2.0.1 * @author sttk3.com * @copyright © 2024 sttk3.com*/ //#target 'illustrator'//@targetengine 'com.sttk3.ai.splitrows' (function() { if(app.documents.length <= 0) {return ;} var doc = app.documents[0] ; var sel = allPageItem(doc.selection) ; if(sel.length <= 0) {return ;} // テキストのみを対象にする var targetItems = filterItems(sel, function(aItem) {return /^TextFrame$/.test(aItem.constructor.name) ;}) ; var itemLength = targetItems.length ; if(itemLength <= 0) {return ;} var newSelection = [] ; var currentItem, createdFrames ; for(var i = itemLength - 1 ; i >= 0 ; i--) { currentItem = targetItems[i] ; switch(currentItem.kind) { case TextType.POINTTEXT: // ポイントテキスト createdFrames = splitRowsPoint(currentItem) ; break ; case TextType.AREATEXT: // エリアテキスト createdFrames = splitRowsArea(currentItem) ; break ; default: // パステキスト continue ; } if(createdFrames.length > 0) { newSelection = newSelection.concat(createdFrames) ; } } // なぜかちゃんと選択してくれないので,空にする→セットする の流れ doc.selection = [] ; doc.selection = newSelection ;})() ; /** * スクリプト実行元アプリケーションのバージョンを取得して数値の配列にする。16.0.4の場合[16, 0, 4] * @return {Number[]}*/function appVersion() { var arr = app.version.toString().split('.') ; var res = [] ; for(var i = 0, len = arr.length ; i < len ; i++) { res.push(Number(arr[i])) ; } return res ;} /** * selectionからgroupItemの中身を含めたすべてのpageItemを返す * @param {Array} sel selection * @return {Array}*/function allPageItem(sel) { var res = [] ; // テキストの中身を選択しているとき,関連するテキストフレームが1つだけの場合それを返す。 // 2つ以上連なるスレッドテキストの場合は諦める if(sel.constructor.name === 'TextRange') { var textFrames = sel.story.textFrames ; if(textFrames.length === 1) { res = [textFrames[0]] ; // 最終的に選択がおかしくなるので対策しておく var aiVersion = appVersion()[0] ; if(24 <= aiVersion) { app.selectTool('Adobe Select Tool') ; } else if((16 <= aiVersion) && (aiVersion <= 23)) { app.executeMenuCommand('deselectall') ; app.selection = res ; } else { // CS6より前のバージョンは,多分TextRangeを選択した状態のままになる } } return res ; } // グループの中身を走査して対象を見つける var currentItem ; for(var i = 0, len = sel.length ; i < len ; i++) { currentItem = sel[i] ; switch(currentItem.constructor.name) { case 'GroupItem' : res.push(currentItem) ; // pageItemsにはconstructor.nameが存在せずエラーになる。Arrayに変換しておく res = res.concat(allPageItem(Array.apply(null, currentItem.pageItems))) ; break ; default : res.push(currentItem) ; break ; } } return res ;} /** * Array.filterみたいなもの * @param {Array} targetItems 対象のArrayかcollection。lengthとindexがあれば何でもいい * @param {Function} callback 条件式 * @return {Array}*/function filterItems(targetItems, callback) { var res = [] ; for(var i = 0, len = targetItems.length ; i < len ; i++) { if(i in targetItems) { var val = targetItems[i] ; if(callback.call(targetItems, val, i)) {res.push(val) ;} } } return res ;} /** * 空白文字しかなければ削除する * @param {TextFrame} targetFrame 対象のtextFrame * @return {Boolean} 削除したかどうか*/function removeEmptyFrame(targetFrame) { var res = false ; if(/^\s*$/.test(targetFrame.contents)) { targetFrame.remove() ; res = true ; } return res ;} /** * textFrameの末尾にある改行文字を削除する * @param {TextFrame} targetFrame 対象のtextFrame * @return {Integer} 削除した行の数*/function trimEnd(targetFrame) { var res = 0 ; for(var i = targetFrame.characters.length - 1 ; i >= 0 ; i--) { if(/[\r\n\x03]/.test(targetFrame.characters[i].contents)) { targetFrame.characters[i].remove() ; res++ ; } else { break ; } } var removed = removeEmptyFrame(targetFrame) ; if(removed) {return Infinity ;} return res ;} /** * targetRangeを新しいtextFrameに複製し,位置を合わせてそのtextFrameを返す * @param {TextFrame} srcFrame ソースのtextFrame * @param {TextRange} srcRange 複製するtextRange * @param {Integer} direction 移動方向。[x, y][direction] * @param {Integer} indexTail 末尾index。[left, top, right, bottom][indexTail] * @return {TextFrame} */function duplicateRange(srcFrame, srcRange, direction, indexTail) { var srcBounds = srcFrame.geometricBounds ; if(!srcBounds) {return ;} // もとのtextFrameの下(右)の座標を記録する var oldTail = srcBounds[indexTail] ; // 文字を新しく生成したtextFrameに複製する var newFrame = srcFrame.duplicate(srcFrame, ElementPlacement.PLACEAFTER) ; newFrame.contents = '' ; srcRange.duplicate(newFrame, ElementPlacement.INSIDE) ; // 位置が合うように移動する var newTail = newFrame.geometricBounds[indexTail] ; var deltaXY = [0, 0] ; deltaXY[direction] = oldTail - newTail ; newFrame.translate(deltaXY[0], deltaXY[1]) ; return newFrame ;} /** * ポイントテキストの行を分割する * @param {TextFrame} targetFrame 分割対象のtextFrame * @return {Array} 分割されたtextFrame*/function splitRowsPoint(targetFrame) { var res = [] ; // ポイントテキストのみを対象とする if(targetFrame.kind !== TextType.POINTTEXT) {return res ;} // 編集するプロパティを定義する var direction, indexTail ; if(targetFrame.orientation === TextOrientation.HORIZONTAL) { // 横組み。Y座標を操作 direction = 1 ; // y indexTail = 3 ; // bottom } else { // 縦組み。X座標を操作 direction = 0 ; // x indexTail = 0 ; // left } // 空白文字しかなければ終了する var removed = removeEmptyFrame(targetFrame) ; if(removed) {return res ;} // 最終行が改行だけだとエラーを起こすので,事前に削除しておく trimEnd(targetFrame) ; // 文字がなくなっていたら終了する removed = removeEmptyFrame(targetFrame) ; if(removed) {return res ;} var rows = targetFrame.paragraphs ; var currentRow, currentText, newFrame ; for(var i = rows.length - 1 ; i >= 1 ; i--) { currentRow = rows[i] ; // 空行または見えない文字だけの行は消して次に進む currentText = currentRow.contents ; if(/^\s*$/.test(currentText)) { currentRow.remove() ; continue ; } // 文字を新しく生成したtextFrameに複製し,位置を合わせる newFrame = duplicateRange(targetFrame, currentRow, direction, indexTail) ; res.unshift(newFrame) ; // 複製し終わった文字を削除する currentRow.remove() ; // textFrameの末尾にある改行文字を(あれば)削除し,その行数分indexを飛ばす i -= trimEnd(targetFrame) ; } // 最後に残ったもとのテキストフレームを,分割済みアイテムの先頭に移動する try { if(targetFrame) { targetFrame.move(res[0], ElementPlacement.PLACEBEFORE) ; res.unshift(targetFrame) ; } } catch(e) { // alert(e) ; } return res ;} /** * テキストのtopとbaselineの差を返す * @param {TextFrame} srcFrame 対象のTextFrame * @param {TextRange} [targetRange] 対象のTextRange。省略時はsrcFrame.lines[0] * @return {Number}*/function getBaselineGap(srcFrame, targetRange) { var res ; if(targetRange == null) {var targetRange = srcFrame.lines[0] ;} var tempGroup ; try { // 作業場を確保する tempGroup = srcFrame.parent.groupItems.add() ; var tempTextFrame = tempGroup.textFrames.pointText([0, 0], TextOrientation.HORIZONTAL) ; targetRange.duplicate(tempTextFrame, ElementPlacement.INSIDE) ; var newPosition = tempTextFrame.position ; res = newPosition[1] ; } finally { if(tempGroup) {tempGroup.remove() ;} } return res ;} /** * TextRange内の指定した属性の中で最も大きい値を取得する * @param {TextRange} targetRange 対象のTextRange * @param {String} attributeName 対象の属性名 * @return {Number}*/function maxCharacterAttribute(targetRange, attributeName) { var max = 0 ; var currentAttribute ; for(var i = 0, len = targetRange.length ; i < len ; i++) { currentAttribute = targetRange.characters[i][attributeName] ; if(currentAttribute > max) {max = currentAttribute ;} } return max ;} /** * 段落ごとに別のTextFrameに分けるとき必要な情報を集めた構造体 * @constructor * @return {Object}*/function TextArea() { return { contents: '', // 文字列(デバッグ用) position: [0, 0], // 原点座標 width: 0, // 列方向の最大サイズ height: 0, // 行方向の最大サイズ textRange: null, // 複製元の参照 orientation: TextOrientation.HORIZONTAL // 組み方向 }} /** * TextFrameからclass TextAreaの配列を生成する * @param {TextFrame} areaTextFrame 対象のTextFrame * @return {Array}*/function createTextAreaDB(areaTextFrame) { var res = [] ; if(areaTextFrame.kind !== TextType.AREATEXT) {return res ;} var patternEmptyRow = /^\s*$/ ; var srcBounds = areaTextFrame.geometricBounds ; // 行送り基準。仮想ボディの上基準の行送り(TopToTop)/欧文ベースライン基準の行送りBottomToBottom // の2種類あるが,縦組みの場合は強制的にTopToTopになる var isTopToTop = true ; var tempTextFrame ; try { // 空行だとプロパティ取得でエラーになり面倒なので,事前にすべての空行に適当な文字を入れておく tempTextFrame = areaTextFrame.duplicate() ; for(var i = tempTextFrame.lines.length - 1 ; i >= 0 ; i--) { if(tempTextFrame.lines[i].contents === '') { tempTextFrame.lines[i].contents = ' ' ; } } var srcText = tempTextFrame.contents ; /* head left □□□□■□ right →column □□□□■□ ↓row tail */ // 位置の情報取得index。[left, top, right, bottom][indexHead] var indexLeft, indexHead, areaWidth, isHorizontal ; var orientation = tempTextFrame.orientation ; if(orientation === TextOrientation.HORIZONTAL) { // 横組み indexLeft = 0 ; // left indexHead = 1 ; // top areaWidth = tempTextFrame.width ; isHorizontal = true ; if(tempTextFrame.textRange.leadingType === AutoLeadingType.BOTTOMTOBOTTOM) { isTopToTop = false ; } } else { // 縦組み indexLeft = 1 ; // top indexHead = 2 ; // right areaWidth = tempTextFrame.height ; isHorizontal = false ; } var currentLeadingLine ; if(isTopToTop) { currentLeadingLine = srcBounds[indexHead] ; } else { currentLeadingLine = srcBounds[indexHead] - getBaselineGap(tempTextFrame, tempTextFrame.lines[0]) ; } var lastTextArea ; var db = [] ; for(var i = 0, rowLength = tempTextFrame.lines.length, lastIndex = rowLength - 1 ; i < rowLength ; i++) { var currentRow = tempTextFrame.lines[i] ; var rowContents = currentRow.contents ; var isEmpty = patternEmptyRow.test(rowContents) ; var rowLeading = 0 ; if(isTopToTop) { rowLeading = maxCharacterAttribute(currentRow, 'leading') ; } else { if(i !== lastIndex) { rowLeading = maxCharacterAttribute(tempTextFrame.lines[i + 1], 'leading') ; } } // 空行かつ前回の続きでない場合,座標の計算だけしてスキップする if(isEmpty && lastTextArea == null) { currentLeadingLine -= rowLeading + currentRow.spaceAfter + currentRow.spaceBefore ; continue ; } /* このTextAreaが扱うpositionやwidthは,横組みのときはIllustrator(ai)と同じ概念。 縦組みのとき,positionはaiでいう[right, top]。widthはcolumn方向の長さ(aiでいうheight),heightはrow方向の長さ(aiでいうwidth)。 後でaiと同じ形式に変換する */ if(lastTextArea == null) { lastTextArea = new TextArea() ; lastTextArea.width = areaWidth ; if(isTopToTop) { lastTextArea.position = [srcBounds[indexLeft], currentLeadingLine] ; } else { var baselineGap = getBaselineGap(tempTextFrame, currentRow) ; lastTextArea.position = [srcBounds[indexLeft], currentLeadingLine + baselineGap] ; lastTextArea.height = baselineGap ; } } // contentsを追加する var rowContents = currentRow.contents ; lastTextArea.contents += rowContents ; // 複製元の参照を追加する var endCode = srcText[currentRow.characterOffset + currentRow.length - 1] ; if(!isEmpty) { // 強制改行があったら範囲に追加する if(endCode === '\x03') {currentRow.length += 1 ;} if(lastTextArea.textRange) { lastTextArea.textRange.length += currentRow.length ; } else { lastTextArea.textRange = currentRow ; } } if( isEmpty || (endCode == null) || (endCode === '\r') || (i === lastIndex) ) { // 段落またはテキスト全体の終わりを示しているとき // 高さを確定し,TextAreaを記録する var characterSize = maxCharacterAttribute(currentRow, 'size') ; if(isTopToTop) { lastTextArea.height += characterSize ; } else { lastTextArea.height += characterSize * 0.12 ; // 0.12は日本語フォント仮想ボディにおけるベースラインより下の部分の比率 } db.push(lastTextArea) ; // 次のcurrentLeadingLineを指定する currentLeadingLine -= rowLeading + currentRow.spaceAfter ; if(i !== 0) {currentLeadingLine -= currentRow.spaceBefore ;} // lastTextAreaをクリアする lastTextArea = null ; } else { // 自動折り返しまたは強制改行のとき // 高さにleadingを追加して次へ lastTextArea.height += rowLeading ; currentLeadingLine -= rowLeading ; } } if(isHorizontal) { // 横書きの場合はそのまま終了する res = db ; } else { // 縦書きの場合はIllustratorの座標に合わせてclass TextAreaの座標を変換する for(var i = 0, len = db.length ; i < len ; i++) { var currentItem = db[i] ; var newItem = TextArea() ; newItem.contents = currentItem.contents ; newItem.position = [currentItem.position[1] - currentItem.height, currentItem.position[0]] ; newItem.width = currentItem.height ; newItem.height = currentItem.width ; newItem.textRange = currentItem.textRange ; newItem.orientation = orientation ; res.push(newItem) ; } } } catch(e) { alert(e) ; } finally { tempTextFrame.remove() ; } return res ;} /** * エリアテキストの行を分割する * @param {TextFrame} targetFrame 分割対象のtextFrame * @return {Array} 分割されたtextFrame*/function splitRowsArea(targetFrame) { var res = [] ; // エリアテキストのみを対象とする if(targetFrame.kind !== TextType.AREATEXT) {return res ;} // 空白文字しかなければ終了する var removed = removeEmptyFrame(targetFrame) ; if(removed) {return res ;} var textAreaDB = createTextAreaDB(targetFrame) ; var textAreaDBLength = textAreaDB.length ; if(textAreaDBLength <= 0) {return res ;} var dstLocation = targetFrame.parent ; for(var i = 0 ; i < textAreaDBLength ; i++) { var currentInfo = textAreaDB[i] ; // 空のTextFrame(エリアテキスト)を生成する var newTextFrame = targetFrame.duplicate() ; newTextFrame.contents = '' ; newTextFrame.width = currentInfo.width ; newTextFrame.height = currentInfo.height ; newTextFrame.position = currentInfo.position ; // 中身を引っ張ってくる currentInfo.textRange.duplicate(newTextFrame, ElementPlacement.INSIDE) ; // 重なり順を調整する newTextFrame.move(targetFrame, ElementPlacement.PLACEBEFORE) ; res.push(newTextFrame) ; } targetFrame.remove() ; return res ;}このサイトで配布しているスクリプトやその他のファイルを,無断で転載・配布・販売することを禁じます。それらの使用により生じたあらゆる損害について,私どもは責任を負いません。スクリプトやファイルのダウンロードを行った時点で,上記の規定に同意したとみなします。
内容の似てるおすすめ記事 SNSでもご購読できます。 広告