Illustratorで年表やグラフなどを作っていると,連続した数値を一定の距離で何回も複製することがあります。例えば,2010年→2015年→2020年といった具合です。
普通にやる場合,以下のような手順になると思います。
- テキストフレームを作って数値を入力する
- フォントやサイズなどのスタイルを整える
- テキストフレームを移動コピーして,次の数値を入力する
- ⌘Dの繰り返し複製などでコピー,入力を繰り返す
とても面倒ですね。
Excelなど表計算ソフトの場合,入力されている2つの値を参考に次の値を作り出す機能があります。いわゆるオートフィルと呼ばれているものです。
もしIllustratorで複製移動したときオートフィルもやってくれたら,半端なく便利ではないでしょうか。
そこで今回は選択した2つテキストフレームの距離でテキストフレームを複製移動し,数値をオートフィルするJavaScriptを紹介します。
以下のIllustrator用スクリプトをダウンロードし,使えるようにしておいてください。
autofillNumber.jsx
使いかた
2つのテキストフレームを選択し,スクリプトを実行するだけです。新しいテキストが生成されます。
実行が終わった後は新しいテキストを含めて2つ選択した状態になります。そのため,⌘Dの繰り返し複製感覚で何回も実行できます。これまた連打必須ですね!
動作説明
古い(背面にある)テキストフレームから新しい(前面にある)テキストフレームに向かう方向と距離で,新しいほうのテキストフレームを複製移動します。
それぞれの内容から数値らしき部分を自動で認識し,見つかった数値のなかで最後のものがオートフィルの対象です。例えば「1_123」という文字があった場合,123が対象になります。数値でない文字があった場合は無視するので,その部分のフォントなどの属性は変わりません。
数値は1,000といった桁区切りつきのもの,2.00のような小数点つきのもの,3のような全角数字,004のような0から始まるものも認識できます。生成するテキストでは,桁区切りのあるなしや小数点以下桁数など,それらの形式を再現するよう努力します。属性は数値の1文字目のものが引き継がれます。
これでまた少し仕事が速くなりました。今日もさっさと仕事を切り上げて好きなことをしましょう!
コードはこちら
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 |
/** * @fileOverview Excelのオートフィル感覚で,選択した2つのテキストフレームから新しい文字を生成する * @author sttk3.com */ //#target 'illustrator' (function() { if(app.documents.length <= 0) {return ;} var doc = app.documents[0] ; // 2つのテキストフレームを対象とする。過不足があれば終了する var sel = allPageItemOfSelection(doc.selection) ; if(sel.length <= 1) {return ;} var targetFrames = filterItems(function(aItem) {return /^TextFrame$/.test(aItem.constructor.name)}, sel) ; if(targetFrames.length != 2) {return ;} targetFrames.reverse() ; // テキストフレームから数値らしき部分を取り出す。なければ終了する。 // 数値のまとまりの中で最後にあるものを対象とする。例えば'図1-12'があれば12が対象になる var reNumber = /(?:(?:^|[^\d])[-−-])?[0-90-9,.]+/g ; // 全角数字を含む大雑把な数値。桁区切りや小数点を含むが,カンマ・小数点のみでもマッチしてしまう var targetRanges = [findRange(reNumber, targetFrames[0]).item(-1), findRange(reNumber, targetFrames[1]).item(-1)] ; if(!targetRanges[0] || !targetRanges[1]) {return ;} // カンマ区切りや小数点以下の桁の形式を確認する。端は特例になりやすいので,端でない方を調べる var strArray = [targetRanges[0][0].contents, targetRanges[1][0].contents] ; var containsComma, containsZenkakuNum, startsWith0 ; var reComma = /,/g ; if(reComma.test(strArray[1])) {containsComma = true ;} // カンマを含むか if(/[0-9]/.test(strArray[1])) {containsZenkakuNum = true ;} // 全角数字を含むか var m ; var menusChar = '\u2212' ; // マイナス符号用文字。全角マイナスがよければ'\uff0d'に if(m = strArray[1].match(/^[-−-]/)) { // マイナス符号が入力してあればそれに合わせる menusChar = m[0] ; } var decimalPlaces = 0 ; // 小数点以下桁数 if(/^[00][0-90-9]+$/.test(strArray[1])) { // 0から始まるinteger(連番)か startsWith0 = true ; var digit = strArray[1].length ; // 連番桁数。例えば001なら3(桁) } else { if(m = strArray[1].match(/\.([0-90-9]+)$/)) { decimalPlaces = m[1].length ; } } // 数値らしき文字列を数値に変換する。NaNなら終了する var numArray = [] ; var currentValue ; for(var i = 0 ; i < 2 ; i++) { currentValue = Number(toHankaku(strArray[i].replace(reComma, '').replace(/^[−-]/, '-'))) ; if(isNaN(currentValue)) {return ;} numArray.push(currentValue) ; } // 次の数値を予測する var newStr = numArray[1] + (numArray[1] - numArray[0]) ; newStr = newStr.toFixed(decimalPlaces) ; if(containsComma) {newStr = addComma(newStr) ;} if(containsZenkakuNum && newStr.length == 1) {newStr = toZenkaku(newStr) ;} if(startsWith0) {newStr = zeroPadding(newStr, digit) ;} // ハイフンをマイナスに置換する newStr = newStr.replace('-', menusChar) ; // 移動距離を算出する。ポイントテキスト同士のときはanchorで,それ以外はpositionで計算する var propName ; if(targetFrames[0].kind == TextType.POINTTEXT && targetFrames[1].kind == TextType.POINTTEXT) { propName = 'anchor' ; } else { propName = 'position' ; } var pos0 = targetFrames[0][propName] ; var pos1 = targetFrames[1][propName] ; var deltaXY = [pos1[0] - pos0[0], pos1[1] - pos0[1]] ; // 新しいテキストフレームを生成して数値をセットする targetFrames.push(targetFrames[1].duplicate()) ; targetFrames[2].translate(deltaXY[0], deltaXY[1]) ; var newRange = targetFrames[2].textRanges[targetRanges[1][0].characterOffset - 1] ; newRange.length = targetRanges[1][0].length ; setContents(newRange, newStr) ; // 連続で実行できるよう選択を変更する doc.selection = [targetFrames[1], targetFrames[2]] ; })() ; /** * selectionからgroupItemの中身を含めたすべてのpageItemを返す * @param {Array} sel selection * @return {Array} */ function allPageItemOfSelection(sel) { var res = [] ; for(var i = 0, len = sel.length ; i < len ; i++) { var currentItem = sel[i] ; switch(currentItem.constructor.name) { case 'GroupItem' : res.push(currentItem) ; res = res.concat(arguments.callee(currentItem.pageItems)) ; break ; default : res.push(currentItem) ; break ; } } return res ; } /** * Array.filterみたいなもの * @param {Function} func 条件式 * @param {Array} targetItems 対象のArrayかcollection。lengthとindexがあれば何でもいい * @return {Array} */ function filterItems(func, targetItems) { var res = [] ; for(var i = 0, len = targetItems.length ; i < len ; i++) { if(i in targetItems) { var val = targetItems[i] ; if(func.call(targetItems, val, i)) {res.push(val) ;} } } return res ; } /** * 検索文字列(正規表現)にマッチした部分のtextRangeを取得する。マッチした場所が位置ならinsertionPointを返す * @param {String | RegExp} searchPattern 検索パターン * @param {TextFrame | TextRange} targetRange 検索対象のtextRange * @return {Object} [[マッチ全体のtextRange, $1のtextRange, $2のtextRange...], ...] 正確に言えば配列でなくオブジェクトを返す * @example * var matchRanges = findRange(/^./gm, doc.textFrames[0]) ; * $.writeln(matchRanges.item(-1)[0].contents) ; * matchRanges.forEach(function(m) { * $.writeln(m[0].contents) ; * }) ; */ function findRange(searchPattern, targetRange) { // 検索対象テキストと検索開始位置を指定 var itemClass = targetRange.constructor.name ; var startIndex, originalStr ; switch(itemClass) { case 'TextFrame' : startIndex = 0 ; originalStr = targetRange.contents ; break ; case 'TextRange' : startIndex = targetRange.characterOffset - 1 ; originalStr = targetRange.contents ; targetRange = targetRange.parent ; break ; } // 強制改行を\nで指定できるように加工する // 今のところ不採用。\nにすると^に反応してしまうので… //originalStr = originalStr.replace('\u0003', '\n') ; // 常にglobalにする if(searchPattern.constructor.name == 'String') {searchPattern = new RegExp(searchPattern, 'g') ;} if(!searchPattern.global) { // 単にsearchPattern.global = trueとしても何も変わらないので新しく作る。 var reOpt = 'g' ; if(searchPattern.ignoreCase) {reOpt += 'i' ;} if(searchPattern.multiline) {reOpt += 'm' ;} searchPattern = new RegExp(searchPattern.source, reOpt) ; } // MatchItemsのconstructor。 MatchItems.item(-1)など負のインデックスに対応するほか, // forEach,reverseEachなどのイテレータを提供する(二次元配列MatchItems[i][0]のような形式は運用が苦痛だった)。 // 文字の置換をするとき,長さが変わって記録した位置がずれると思いreverseEachを作ったもののforEachでも普通にできるようだ。 // 個々のMatchItemはオブジェクトを作らず,[マッチ全体, m[1], m[2], ...m[n]]の配列にする var MatchItems = function MatchItems() { this.length = 0 ; } ; MatchItems.prototype.forEach = function(func) { for(var i = 0, len = this.length ; i < len ; i++) { func.call(this, this[i], i) ; } } ; MatchItems.prototype.reverseEach = function(func) { for(var i = this.length - 1 ; i >= 0 ; i--) { func.call(this, this[i], i) ; } } ; MatchItems.prototype.item = function(keyNumber) { var res ; if(keyNumber >= 0) { res = this[keyNumber] ; } else { res = this[this.length + keyNumber] ; } return res ; } ; // 検索を実行 var res = new MatchItems() ; var matchObjIndex = 0 ; var matchObj, matchObjLength, m0Index, m0Length, m0Range, currentMatch ; var currentIndex, captureIndex, tempIndex, mnIndex, mnLength, mnRange ; try { while(matchObj = searchPattern.exec(originalStr)) { matchObjLength = matchObj.length ; m0Index = matchObj.index + startIndex ; //m[0]開始位置 m0Length = matchObj[0].length ; //m[0]の文字数 // マッチした部分が文字列ならtextRangeを,位置ならinsertionPointを作る if(matchObj[0] == '') { // マッチした部分が位置ならinsertionPointを作る m0Range = targetRange.insertionPoints[m0Index] ; // マッチした部分のlengthが0だと同じ場所でループし続けるので,次のマッチ開始位置を明示的に指定する searchPattern.lastIndex += 1 ; } else { // 文字列ならtextRangeをを作る m0Range = targetRange.textRanges[m0Index] ; m0Range.length = m0Length ; //m[0]のtextRange } currentMatch = [] ; currentMatch.push(m0Range) ; currentIndex = 0 ; //m[n]のtextRangeを作る。m[0]の文字列からindexOfで位置を出しているので,m[n]が空文字列だと働かない for(var i = 1 ; i <= matchObjLength ; i++) { if(matchObj[i]) { tempIndex = matchObj[0].indexOf(matchObj[i], currentIndex) ; mnIndex = m0Index + tempIndex ; //m[n]の開始位置 mnLength = matchObj[i].length ; //m[n]の文字数 mnRange = targetRange.textRanges[mnIndex] ; if(mnLength > 0) { mnRange.length = mnLength ; //m[n]のtextRange } currentIndex = tempIndex + mnLength ; //次のキャプチャ検索開始位置 currentMatch.push(mnRange) ; } } res[matchObjIndex] = currentMatch ; matchObjIndex++ ; } res.length = matchObjIndex ; } catch(e) { alert(e) ; return ; } finally { // 次のマッチ開始位置は明示的に戻さないと設定が引き継がれてしまうようだ。必ず元に戻すこと searchPattern.lastIndex = 0 ; } return res ; } /** * 全角英数を半角にする * @param {String} str 対象の文字 * @return {String} */ function toHankaku(str) { var res = str.replace(/[A-Za-z0-9_]/g, function(s) {return String.fromCharCode(s.charCodeAt(0) - 0xFEE0)}) ; return res ; } /** * 半角英数を全角にする * @param {String} str 対象の文字 * @return {String} */ function toZenkaku(str) { var res = str.replace(/[A-Za-z0-9_]/g, function(s) {return String.fromCharCode(s.charCodeAt(0) + 0xFEE0)}) ; return res ; } /** * 数値を3桁区切りにする * @param {Number} num 対象の数値 * @return {String} */ function addComma(num) { return num.toString().replace(/^(-?[0-9]+)(?=\.|$)/, function(s) {return s.replace(/([0-9]+?)(?=(?:[0-9]{3})+$)/g, '$1,') ;}) ; } /** * 数値の頭に指定の桁数で0を追加する * @param {Number} num 対象の数字 * @param {Number} digit 桁数 * @return {String} */ function zeroPadding(num, digit){ return (Array(digit).join('0') + num).slice(-digit) ; } /** * 対象のtextRangeにcontentsをセットする。後ろの文字の属性を引き継がないための機能 * @param {TextRange} targetRange 対象のtextRange * @param {String} str セットするテキスト * @return なし */ function setContents(targetRange, str) { var oldLength = targetRange.contents.length ; // 後ろの文字属性の影響を切るため,対象のtextRangeより前に文字を挿入する var charOffset = targetRange.characterOffset ; var tempRange = targetRange.story.insertionPoints[charOffset - 1].characters.add(str) ; // もとの文字を削除する var deleteRange = targetRange.story.textRanges[targetRange.characterOffset + str.length - 1] ; deleteRange.length = oldLength ; deleteRange.remove() ; } |
このサイトで配布しているスクリプトやその他のファイルを,無断で転載・配布・販売することを禁じます。
それらの使用により生じたあらゆる損害について,私どもは責任を負いません。
スクリプトやファイルのダウンロードを行った時点で,上記の規定に同意したとみなします。