【解決】Illustratorでもテキストを行ごとに分割したい!
【解決】Illustratorでもテキストを行ごとに分割したい!

【解決】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 ;}

このサイトで配布しているスクリプトやその他のファイルを,無断で転載・配布・販売することを禁じます。それらの使用により生じたあらゆる損害について,私どもは責任を負いません。スクリプトやファイルのダウンロードを行った時点で,上記の規定に同意したとみなします。

 内容の似てるおすすめ記事
  • 【解決】さまざまな図法で書き出せるベクター世界地図制作アプリがほしい!
  • 【解決】Figmaにも変形パネルがほしい!
  • 【まとめ】minimumAreaサブスクリプション版
  • 【まとめ】infoVectorサブスクリプション版
SNSでもご購読できます。 広告
📎📎📎📎📎📎📎📎📎📎
BOT