ごんれのラボ

iOS、Android、Adobe系ソフトの自動化スクリプトのことを書き連ねています。

MacにAdobe CEPの開発環境を構築する with Visual Studio Code

概要

Adobeアプリケーションのエクステンション(CEP:Common Extensibility Platform)をMacで開発するための開発環境を構築する。
過去にBracketsを利用した開発環境構築の記事を書いたが、Adobe公式のVisual Studio CodeプラグインであるExtendScript Debuggerが発表されたので、開発環境をVisual Studio Codeに移行することにし、記事にまとめることにした。

この記事でやること

  • 開発環境について
  • 開発環境の構築について
  • サンプルエクステンションの作成・起動について

この記事でやらないこと

  • エクステンションの開発について

想定する環境

  • macOS 10.13.6
  • Adobe Creative Cloudの2015以降のアプリケーションがインストール済

開発環境について

開発環境の選定

概要でも述べたがAdobeが今後のスクリプト開発の環境としてVisual Studio Codeを選択した(気まぐれかもしれないが)ので、開発環境はVisual Studio Codeとなる。
Visual Studio CodeはBrackets以上に開発を効率化してくれるプラグインが豊富にあるため、HTML5+CSS+JavaScriptで構成されているCEPの開発にはもってこいだろう。

Visual Studio Codeとは

Visual Studio Codeは、Microsoftが開発したオープンソースのコードエディタ。
azure.microsoft.com

拡張機能が豊富で、自分の使いたい機能を追加、もしくは開発して公開することが可能である。
どんな機能拡張があるかは、下記のサイトを参照。
marketplace.visualstudio.com

開発環境の構築

ここからは実際に開発環境を構築していく手順を紹介する。

Visual Studio Codeのインストール

前述した公式サイトからダウンロードできる。

  1. 公式サイトから最新版のzipをダウンロードする
  2. ダウンロードしたzipを展開したらそのフォルダ内に Visual Studio Code.app が現れる
  3. アプリケーションをApplicationsディレクトリにコピーする
  4. コピーしたアプリケーションを起動する

Visual Studio Code拡張機能の CC Extension Builder をインストール

marketplace.visualstudio.com

CC Extension Builder は、簡単にCEPのテンプレートプロジェクトを作成することができるプラグイン。

  1. Visual Studio Codeのサイドバーメニューの機能拡張を選択し、検索フィールドにCC Extension Builderと入力する
  2. 検索フィールドで「CEP」と入力、表示された CC Extension Builder をクリックする
  3. Visual Studio Code上で CC Extension Builder のページが開くので、インストールボタンをクリック
  4. さきほどクリックしたインストールボタンがアンインストールボタンになっていればインストール成功

Player Debug Mode の設定

CEPをアプリケーションにインストールするには、本来はCEPのプロジェクトをパッケージしたzxpをインストールする必要がある。
そしてパッケージするためには、証明書が必要だったり、他ツールのインストールが必要だったりするんだけど、その準備がなかなか面倒くさく、かつデバッグのたびにパッケージするのも手間だ。
そこで、デバッグモードを有効にする。

まず、メニューから 表示 > コマンド パレット... を選択して、コマンドパレットを表示する。ショートカットは cmd + shift +p だ。
表示されたパレットの検索フィールドに「cep debug」と入力し、絞り込まれた選択肢の中から「Extension Creator: Enable CEP Debug Mode」を選択し、デバッグモードを有効にする。

デバッグモードを有効にすることでインストールする手間がなくなり、所定のディレクトリにCEPを設置することでデバッグ実行が可能になる。
作成したCEPのプロジェクトを以下のディレクトリに設置し、対象のアプリケーションを再起動すると、アプリケーションに作成したCEPが読み込まれる。

f:id:macneko-ayu:20180529162956p:plain
公式ドキュメントより抜粋

サンプルエクステンションの作成・起動について

CEPの開発環境が構築できているか、サンプルエクステンションを作成して確認する。

サンプルエクステンションの作成手順

  1. メニューから 表示 > コマンド パレット... を選択して、コマンドパレットを表示する
  2. 表示されたパレットの検索フィールドに「create cc」と入力し、絞り込まれた選択肢の中から「Extension Creator: Create a New CC Extension」を選択する
  3. 作成するExtension IDを入力するパレットが表示される。Extension IDはドメインを逆転させた形式となり、デフォルトでは com.example.helloworld と入力されているので、任意のIDを入力してEnterを押す
  4. 続いて作成するExtensitonの名前を入力するパレットが表示される。任意の名前を入力してEnterを押す
  5. 最後に作成するテンプレートの形式を選択する。「basic」「topcoat」「spectrum」「theme」の4種類が選択できる。各テンプレートにどんな差があるかは調べられていないので VSCodeで始めるAdobe CEP Extension開発 を読むと良さそう
  6. 以下のパスにサンプルエクステンションが作成されている。設定は次の項で行う
/Users/(user_name)/Library/Application Support/Adobe/CEP/extensions/com.example.helloworld

サンプルエクステンションの設定

作成したサンプルエクステンションの設定を変えて、InDesignで利用できるようにする。
変更するところは CSXS/manifest.xml にある以下の3つの項目。

  • InDesignのバージョン表記行
  • CEPのバージョン × 2ヶ所

前項で作成したサンプルエクステンションがVisual Studio Codeで開かれていると思うので、CSXS/manifest.xml を開く。

まず、InDesingのバージョン表記行を変更する。
作成された状態ではPhotoshopのみ有効になっているので、InDesignでの読み込み部分のコメントアウトを解除する。行頭の <!-- と行末の --> を削除するか、ショートカットの cmd + / でコメントアウトが解除できる。
そのあとは、エクステンションを利用できるInDesignのバージョンを変更する。 Version="[10.0,99.9]"[10.0,99.9] が「バージョン番号10から99.9まで対象」という意味になっている。
ここではInDesign CC 2018で利用するためにバージョンを [10.0,13.1] に変更する。

続いて、CEPのバージョンを変更する。
<ExtensionManifest Version="9.0" と記載されているところを探して、9.0 の部分を 7.0 に変更する。
そのあと <RequiredRuntime Name="CSXS" Version="9.0" /> と記載されているところを探して、9.0 の部分を 7.0 に変更する。

以上でInDesign 2018で利用できるように設定できた。
変更箇所を以下にまとめておく。

変更前

<!-- <Host Name="IDSN" Version="[10.0,99.9]" /> -->
<ExtensionManifest Version="9.0"
<RequiredRuntime Name="CSXS" Version="9.0" />

変更後

<Host Name="IDSN" Version="[10.0,13.1]" />
<ExtensionManifest Version="7.0"
<RequiredRuntime Name="CSXS" Version="7.0" />

サンプルエクステンションを利用する

  1. InDesign CC 2018を起動する
  2. メニューから ウィンドウ>エクステンション>Hello World を選択
  3. パネルが表示されるので、Call ExtendScript ボタンをクリック
  4. アラートが表示され、hello from ExtendScript というメッセージが表示されていれば成功

注意点

CEPが読み込まれない場合は、設定が誤っている可能性が高い。
簡単なチェックリストを紹介する。

  • Player Debug Mode が有効になっているか
  • 利用したいアプリケーションが manifest.xml でコメントアウトされていないか、もしくは記載が削除されていないか
  • 利用したいアプリケーションのバージョンが manifest.xml に含まれているか

公式ドキュメント

アプリケーションのIDや対応しているCEPのバージョン、各アプリケーションのCEP上でのバージョンなどはGitHubのリポジトリから参照できる。 github.com

最後に

出遅れた感はあるが、Visual Studio CodeでCEP開発を行うための環境構築について簡単にまとめた。
筆者自身は最近CEPを書いていないので情報が不足している部分も多いと思うので、このブログを参照して不足部分を補った記事を公開して筆者に教えていただけると、とても嬉しい。

+DESIGNING vol.47の「ラクラボ。/知れば知るほどラクになるスクリプトのススメ[AppleScript編]」の執筆を担当しました

概要

本日(2019/03/29)発売の+DESIGNING vol.47の企画ページ、「ラクラボ。/知れば知るほどラクになるスクリプトのススメ[AppleScript編]」の執筆を担当をしました。

公式サイトは以下。
www.plus-designing.jp

サイトでは各企画の見開き状態の画像が見られ、私が執筆したラクラボ。も一見開き分だけ見られます。

書店、Amazonなどでぜひお買い求めください。

きっかけ

+DESIGNINGの編集長である小林さんと著者陣の一人である村上さんから、「AppleScriptについての記事をラクラボ。で書いてみませんか?」とお話をいただいたことがきっかけでした。
いままで編集はしたことがあっても商業誌の原稿を書くという経験はなかったので、二つ返事でお請けしました。

記事の内容

ターゲット層を絞る必要があり、スクリプト初心者向けの導入となる内容にするか、一歩先に進んだ初級〜中級者向けの内容にするか、かなり悩みました。
vol.46のラクラボ。で、友人の id:uske-s さんがスクリプトの導入的な記事を書いており、そこから先に進むための後押しができるような内容にもしたいと思いました。
最終的にはスクリプト初心者向けの内容にしたのですが、機会があればAppleScriptとJavaScriptで同じ処理を行うスクリプトを書いて比較するといったものも書いてみたいですね(需要があるのか…)

執筆の進め方

原稿はDropbox Paperで適宜確認できる形で進めるという方針でした。

私はいきなり文章を書くことができないタイプなので、登壇資料と同様にMarkdownでざっくりと草案を書くことにしました。

書きたいことに沿った適当な見出し、たとえば「AppleScriptとは」「短めのサンプルコード書く」などといったものでブロック分けして、そのあと文章を意識しないで箇条書きでざっと要素を洗い出しました。
その草案内にキャプチャを差し込んだり、サンプルコードを差し込んだりして、全体の内容を決めます。
そのあと見直しを行って粗目に編集、たとえばてにをはだったり、誤字脱字の調整作業を行います。 これで草案は完成なので、この時点のものを編集さんに確認していただきました。

最後に草案をもとに文章化して、構成のおかしいところの調整作業を行って完成です。

編集さんにはひと手間おかけてしまいましたが、草案の時点で指摘をしていただくことで精度をあげられたと思っています。

最後に

執筆の機会を与えていただき、小林さん、村上さん、ありがとうございました。

また、この記事を読んだ方がAppleScript…じゃなくてもいいのでスクリプトに興味をもっていただけると幸いです。
AppleScriptは独特な構文ということもあり敬遠される方も多いかもしれませんが、Macを使うならば使えるようになって損はしないと思います。

読んでくれたみなさんもAppleScriptの記事を書いてくれよな!

おまけ

DTPerのスクリプトもくもく会もよろしくお願いします!

dtpmkmk.connpass.com

明日3/30にもイベントを開催しますので、そちらもぜひご参加ください!

dtpmkmk.connpass.com

ExtendScript Debugger + TypeScript で ExtendScript を書くためのテンプレートを作った

概要

ExtendScript DebuggerというVisual Studio CodeプラグインがAdobeからリリースされた。
いままで利用されていたExtendScript Toolkitの開発が凍結されたので、以降の開発はこのExtendScript Debuggerを利用して行うことになった。
その都度環境を作るのが面倒で自分用に雑にテンプレートを作ったので、ついでにGitHubで公開することにした。

ExtendScript Debuggerの導入

Adobe Tech BlogのExtendScript Debugger for Visual Studio Code Public Release を読む。
そのあと、id:uske_S さんのExtendScript Debugger for VSCodeがリリースされたので簡単にまとめてみたを読む。

仕様

  • 型定義としてten-A/Types-for-Adobeを利用した
  • TypeScriptで書けるようにした
  • 各種configは暫定的な内容になっている。随時更新予定

使い方

$ npm install -g typescript # when not installed typescript
$ git clone git@github.com:macneko-ayu/template-for-extendscript-using-typescript.git
$ cd template-for-extendscript-using-typescript
$ npm install 
$ npm run watch

src/app.ts を変更して保存すると、dist/ の中に app.js が書き出される。
書き出された app.js をVisual Studio Codeで実行するか、アプリケーションから実行する。

ソースコード

https://github.com/macneko-ayu/template-for-extendscript-using-typescript

Swiftで文字列の輪郭に線をつける(UIBezierPath版)

概要

Swiftで文字列の輪郭に線をつける という記事を書いたんだけど、仕上がりに納得がいかなかったので別のアプローチで実現してみた。

ゴール

Illustratorのアピアランス機能で作って画像化したものと近い感じにする。

illustrator

問題点

  • NSAttributedStringattributes で直接線をつけていたため、線の太さに応じて塗りの範囲が狭くなってしまった
  • 文字が角ばり、丸くできない

実現方法

文字を UIBezierPath に変換して描画するようにした。
実装は 【iOS】テキストの輪郭パスを取得する を参考にさせていただき、Swiftに書き直した。
あわせて TextOutlineShapeView というカスタムViewを作って、その中で描画するようにした。
構造としては文字列を UIBezierPath に変換して、そのPathをセットした CAShapeLayer を3つ重ねて、それぞれ「塗り」「線」「影」の設定を適用した。

TextOutlineShapeView の実装は以下の通り。

import UIKit

class TextOutlineShapeView: UIView {
    struct TextOptions {
        let text: String
        let font: UIFont
        let lineSpacing: CGFloat
        let textAlignment: NSTextAlignment
    }

    struct ShapeOptions {
        let lineJoin: CAShapeLayerLineJoin
        let fillColor: CGColor
        let strokeColor: CGColor
        let lineWidth: CGFloat
        let shadowColor: CGColor
        let shadowOffset: CGSize
        let shadowRadius: CGFloat
        let shadowOpacity: Float
    }

    private var textOptions: TextOptions?
    private var shapeOptions: ShapeOptions?

    init(textOptions: TextOptions, shapeOptions: ShapeOptions) {
        self.textOptions = textOptions
        self.shapeOptions = shapeOptions
        super.init(frame: .zero)
        configure()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
}

extension TextOutlineShapeView {
    private func configure() {
        guard let textOptions = self.textOptions,
            let shapeOptions = self.shapeOptions else {
                return
        }
        let textArray = textOptions.text.components(separatedBy: "\n")
        let attibutes: [NSAttributedString.Key: Any] = [.font: textOptions.font]
        let attibutedStrings = textArray.map { text -> NSAttributedString in
            return NSAttributedString(string: text, attributes: attibutes)
        }
        guard let bezier = makePathfromText(attibutedStrings: attibutedStrings, lineSpacing: textOptions.lineSpacing, textAlignment: textOptions.textAlignment) else { return }
        bezier.flip(direction: .y)

        let shadowLayer = CAShapeLayer()
        shadowLayer.lineJoin = shapeOptions.lineJoin
        shadowLayer.frame = self.bounds
        shadowLayer.shadowPath = bezier.cgPath
        shadowLayer.shadowColor = shapeOptions.shadowColor
        shadowLayer.shadowOffset = shapeOptions.shadowOffset
        shadowLayer.shadowRadius = shapeOptions.shadowRadius
        shadowLayer.shadowOpacity = shapeOptions.shadowOpacity
        self.layer.addSublayer(shadowLayer)

        let strokeLayer = CAShapeLayer()
        strokeLayer.lineJoin = shapeOptions.lineJoin
        strokeLayer.frame = self.bounds
        strokeLayer.strokeColor = shapeOptions.strokeColor
        strokeLayer.lineWidth = shapeOptions.lineWidth
        strokeLayer.path = bezier.cgPath
        self.layer.addSublayer(strokeLayer)

        let fillLayer = CAShapeLayer()
        fillLayer.lineJoin = shapeOptions.lineJoin
        fillLayer.frame = self.bounds
        fillLayer.fillColor = shapeOptions.fillColor
        fillLayer.path = bezier.cgPath
        self.layer.addSublayer(fillLayer)
    }

    private func makePathfromText(attibutedStrings: [NSAttributedString], lineSpacing: CGFloat, textAlignment: NSTextAlignment) -> UIBezierPath? {
        let path = UIBezierPath()
        path.move(to: .zero)

        var maxWidth: CGFloat = 0
        for attibutedString in attibutedStrings {
            let line = CTLineCreateWithAttributedString(attibutedString)
            let rect = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
            let width = rect.width

            if maxWidth < width {
                maxWidth = width
            }
        }

        for (i, attibutedString) in attibutedStrings.reversed().enumerated() {
            let letters = CGMutablePath()

            let line = CTLineCreateWithAttributedString(attibutedString)
            let rect = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
            let width = rect.width

            var margin: CGFloat = 0
            switch textAlignment {
            case .center:
                margin = (maxWidth - width) / 2
            case .right:
                margin = maxWidth - width
            default:
                break
            }

            let runArray: [CTRun] = cfArraytoArray(sourceArray: CTLineGetGlyphRuns(line))
            for run in runArray {
                let fontPointer = CFDictionaryGetValue(CTRunGetAttributes(run), Unmanaged.passUnretained(kCTFontAttributeName).toOpaque())
                let runFont = unsafeBitCast(fontPointer, to: CTFont.self)

                for index in 0..<CTRunGetGlyphCount(run) {
                    let thisGlyphRange = CFRange(location: index, length: 1)
                    var glyph = CGGlyph()
                    var position = CGPoint()
                    CTRunGetGlyphs(run, thisGlyphRange, &glyph)
                    CTRunGetPositions(run, thisGlyphRange, &position)

                    guard let letter = CTFontCreatePathForGlyph(runFont, glyph, nil) else { continue }
                    let px = position.x + margin
                    let py = position.y + path.bounds.height + ((i == 0) ? 0 : lineSpacing)
                    let t = CGAffineTransform(translationX: px, y: py)
                    letters.addPath(letter, transform: t)
                }
            }

            path.append(UIBezierPath(cgPath: letters))
        }

        return path
    }

    private func cfArraytoArray<T>(sourceArray: CFArray) -> [T] {
        var destinationArray = [T]()
        let count = CFArrayGetCount(sourceArray)
        destinationArray.reserveCapacity(count)
        for index in 0..<count {
            let untypedValue = CFArrayGetValueAtIndex(sourceArray, index)
            let value = unsafeBitCast(untypedValue, to: T.self)
            destinationArray.append(value)
        }
        return destinationArray
    }
}

最初実装したときになぜか上下反転して表示されてしまった。
why

transformがうまくいっていなかったようなので、 UIBezierPathのextensionを実装した。
extensionの実装は以下の通り。

import UIKit

extension UIBezierPath {
    enum InvertDirection {
        case none
        case x
        case y
        case both
    }

    func flip(direction: InvertDirection) {
        let rect = self.bounds
        switch direction {
        case .none:
            break
        case .x:
            self.apply(CGAffineTransform(translationX: -rect.origin.x, y: 0))
            self.apply(CGAffineTransform(scaleX: -1, y: 1))
            self.apply(CGAffineTransform(translationX: rect.origin.x + rect.width, y: 0))
        case .y:
            self.apply(CGAffineTransform(translationX: 0, y: -rect.origin.y))
            self.apply(CGAffineTransform(scaleX: 1, y: -1))
            self.apply(CGAffineTransform(translationX: 0, y: rect.origin.y + rect.height))
        case .both:
            self.apply(CGAffineTransform(translationX: -rect.origin.x, y: -rect.origin.y))
            self.apply(CGAffineTransform(scaleX: -1, y: -1))
            self.apply(CGAffineTransform(translationX: rect.origin.x + rect.width, y: rect.origin.y + rect.height))
        }
    }
}

使い方

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // For example
        let str = "我が家の猫は7.4kg\nゆえに重い"
        guard let font = UIFont(name: "HiraKakuProN-W6", size: 36) else { return }

        let textOptions = TextOutlineShapeView.TextOptions(text: str,
                                                           font: font,
                                                           lineSpacing: 10,
                                                           textAlignment: .left)
        let shapeOptions = TextOutlineShapeView.ShapeOptions(lineJoin: .round,
                                                             fillColor: UIColor.white.cgColor,
                                                             strokeColor: UIColor.blue.cgColor,
                                                             lineWidth: 5,
                                                             shadowColor: UIColor.black.cgColor,
                                                             shadowOffset: CGSize(width: 3, height: 3),
                                                             shadowRadius: 5,
                                                             shadowOpacity: 0.6)
        let shapeView = TextOutlineShapeView(textOptions: textOptions, shapeOptions: shapeOptions)
        self.view.addSubview(shapeView)

        shapeView.translatesAutoresizingMaskIntoConstraints = false
        let top = shapeView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 10)
        let bottom = shapeView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: 10)
        let leading = shapeView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor, constant: 10)
        let trailing = shapeView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: 10)
        NSLayoutConstraint.activate([top, bottom, leading, trailing])
    }
}

sample

いい感じ。
実用するなら描画後に画像化したほうがパフォーマンスがいいと思う。

大きく表示したサンプル

角の処理結果が見づらいので、文字サイズを大きくした。

NSAttributedString で作ったもの。美しくない…。
bad

UIBezierPath で作ったもの。美しい。
good

ソースコード

https://github.com/macneko-ayu/TextOutlineShapeView

Swiftで文字列の輪郭に線をつける

概要

Swiftで袋文字を実現したいという話を聞いて、調べたら実現方法がさっくり見つかったので残しておくことにした。

実現方法

UILabelextension を定義して、そのメソッドを使うだけ。
参考記事のままだとエラーになるのでよしなに修正してある。

extension UILabel{

    /// makeOutLine
    ///
    /// - Parameters:
    ///   - strokeWidth: 線の太さ。負数
    ///   - oulineColor: 線の色
    ///   - foregroundColor: 縁取りの中の色
    func makeOutLine(strokeWidth: CGFloat, oulineColor: UIColor, foregroundColor: UIColor) {
        let strokeTextAttributes = [
            .strokeColor : oulineColor,
            .foregroundColor : foregroundColor,
            .strokeWidth : strokeWidth,
            .font : self.font
        ] as [NSAttributedString.Key : Any]
        self.attributedText = NSMutableAttributedString(string: self.text ?? "", attributes: strokeTextAttributes)
    }
}

見た目があまり美しくないので、静的な用途であればIllustratorなどのアプリケーションでSVG書き出ししたものを使うと良い。

Playground

Playgroundにコードをコピペして実行するとどんな感じが確認できる。

import UIKit

extension UILabel{

    /// makeOutLine
    ///
    /// - Parameters:
    ///   - strokeWidth: 線の太さ。負数
    ///   - oulineColor: 線の色
    ///   - foregroundColor: 縁取りの中の色
    func makeOutLine(strokeWidth: CGFloat, oulineColor: UIColor, foregroundColor: UIColor) {
        let strokeTextAttributes = [
            .strokeColor : oulineColor,
            .foregroundColor : foregroundColor,
            .strokeWidth : strokeWidth,
            .font : self.font
        ] as [NSAttributedString.Key : Any]
        self.attributedText = NSMutableAttributedString(string: self.text ?? "", attributes: strokeTextAttributes)
    }
}

let label = UILabel(frame: CGRect(x: 0, y: 0, width: 500, height: 60))
label.font = UIFont.boldSystemFont(ofSize: 50)
label.text = "我が家の猫は7.4kg"
label.makeOutLine(strokeWidth: -2.0, oulineColor: .white, foregroundColor: .blue)

キャプチャ

uilabel-outline

おまけ

NSAttributedString.Key でいろいろ装飾できるので、ドキュメントを読むと面白い。
NSAttributedString.Key

iOSでチラシレイアウトも実現できる。需要はない。
iOSでチラシっぽい価格レイアウトを再現してみた

参考

元ネタ

Custom Label Effects in Swift 4

個人的に毎度読むスライド

最高
Mastering Textkit

【実績紹介】デジタル・アド・サービス様 InDesign用スクリプト納品

概要

デジタル・アド・サービス様にInDesign用のスクリプトを2案件納品させていただきましたので、問題のない範囲でどういったスクリプトか、工夫した点はどこかなどをご紹介します。
また汎用的に使えそうな部分(&コアじゃない部分)の処理の一部はスクリプトを記載しています。
なお、この記事の内容はデジタル・アド・サービス様の許可をいただいています。

どういったスクリプトを納品したか

どちらのスクリプトもカタログの組版データから、必要な情報をテキストファイルに書き出すというものです。
ひとつは表組内の製品番号と同行にある価格を走査して製品番号と一緒にテキストファイルに抜き出し、もうひとつは製品番号が含まれる表組のページ内の面積占有率を計算してテキストファイルに抜き出します。

工夫した点

製品番号をページ内からどう検索するかについて

この処理は製品番号が含まれているテキストフレームの構造を理解する必要がありました。
該当するテキストフレームはページ内に存在し、かつ表組みが含まれているものであると仮定して、その条件にマッチするテキストフレームを抽出し、配列化しました。
また、あわせてページ内のテキストフレームを左上から右下に向かって並び替える処理も行っています。

実装したスクリプトの一部を以下に掲載します。

/**
 * Tableを含むTextFrameを抽出
 * @param  {Array} txFrames TextFrameの配列
 * @return {Array}          TextFrameの配列
 */
function extractTxFrameContainingTable(txFrames) {
    var result = [];
    for (var i = 0, txFrameLen = txFrames.length; i < txFrameLen; i++) {
        if (txFrames[i].tables.length > 0) {
            result.push(txFrames[i]);
        }
    }
    return result.length > 0 ? result : undefined;
}

/**
 * pageオブジェクト内のTextFrameを抽出して座標順にソート
 * @param  {Page} currentPage Pageオブジェクト
 * @return {Array}            TextFrameの配列
 */
function extractTxFrameInPage(currentPage) {
    var tfsArray = [];
    for (var i = 0, len = currentPage.allPageItems.length; i < len; i++) {
        var pageItem = currentPage.allPageItems[i];
        if (pageItem.constructor.name == 'TextFrame') {
            tfsArray.push(pageItem);
        }
    }
    return sortObjectsByCoordinateOrder(tfsArray);

    /**
     * 座標順(昇順)にソート
     * @param  {Collection} objects 座標値をもつオブジェクトのコレクション
     * @return {Array}              ソートした配列
     */
    function sortObjectsByCoordinateOrder(objects) {
        var sorted = objects.concat();
        sorted.sort(function (a, b) {
            // Y軸比較
            if (a.geometricBounds[0] < b.geometricBounds[0]) {
                return -1;
            }
            if (a.geometricBounds[0] > b.geometricBounds[0]) {
                return 1;
            }
            // X軸比較
            if (a.geometricBounds[1] > b.geometricBounds[1]) {
                return 1;
            }
            if (a.geometricBounds[1] == b.geometricBounds[1]) {
                return 0;
            }
            return -1;
        });
        return sorted;
    }
}

製品番号から価格を走査する処理について

この処理はInDesignの機能である正規表現検索と、表組のセル内のテキストを走査することで実現しました。
ここの処理がこの案件の要ともいえる部分で、結構苦労しましたし、何度かゼロから書き直したりしました。

正規表現検索

製品番号や価格は一定のルールで構成されているため、そのルールに則り正規表現のパターンをいくつか用意して配列化し、その配列分の検索を繰り返して検索を行いました。
また、その際に精度をあげるため段落スタイルも検索条件に加えました。こちらはドキュメント内のすべての段落スタイルの中から、製品番号や価格に使われているであろうスタイル名をもとに抽出して配列化しています。スタイルグループにも対応しました。

表組のセル内のテキスト走査

「製品番号と価格が同行にある価格を抜き出す」という条件があったので、それを実現するために製品番号を上述した正規表現検索で検索して、ヒットした製品番号が含まれているセルから右にあるセルを順番に処理し、その中に価格が含まれていたら抽出する処理を実装しました。
また、追加要件として製品番号と同行に価格がなかったら次の行にある価格を走査して抽出するという要望があったので、そちらもさくっと実装しました。

面積計算の前準備スクリプトについて

製品番号ごとの面積占有率を計算するにあたりオペレーターの作業が必要になるため、前準備スクリプトを実装しました。
このスクリプトは既存のドキュメントに対して以下のような処理を行います。

  • 段落スタイルを作成
  • レイヤーを作成
  • オブジェクトスタイルを作成
  • スウォッチを作成
  • フレームをひとつ作成して、オブジェクトスタイルとスウォッチを適用

このスクリプトのルールに則っていないフレームは処理の対象外とすることとし、後述する面積計算の処理をどのフレームに対して行えばよいかを明確にできました。

面積占有率計算について

ページ内の特定のレイヤーに存在する特定のオブジェクトスタイルが適用されているフレームを対象に、ページ内の面積占有率を計算しました。
また、上記のフレームの上に重なるように採番用のIDを流し込んだテキストフレームを作成し、IDは面積占有率やページ内の何番目のフレームかがわかるように、スクリプト内で自動生成しました。

その他

ワークフローや仕様要件の提案

対面やSlackでの打ち合わせをしたときに既存のワークフローをお聞きして、スクリプトでどう効率化するのかの提案や仕様要件の提案を行いました。
とはいえ、スクリプトを書ける担当者様からのご依頼だったので、

担「こんな風にスクリプトでできると思うのですが可能ですか?」
私「できますね。難しいとおっしゃってたこのパターンもこういう条件なら対応できますが、そういう処理で大丈夫ですか?」
担「いいですね!それでいきましょう!」

といった感じにスムーズに提案を受け入れていただいて、手戻りがあまり発生せずに進めることができました。
また、仕様要件や成果物であるテキストファイルの仕様、使い方をドキュメントとして納品しました。

処理の共通化

二案件同時にご依頼があったこともあり、処理の共通化を念頭において仕様要件を決めました。
結果的に製品番号を検索する処理、テキストを抽出する処理、レイヤーのロックを解除する処理、テキストファイルを保存する処理、スタイルを検索する処理と、ほとんどの処理を共通化できました。
ライブラリとしてincludeすることも検討したのですが、スクリプト単体で運用できるほうが取り回しやすいだろうということで、ライブラリ化は見送りました。

Undoの履歴に残らないようにした

大量の処理を行うので、Undoの履歴に残らないように doScript() を利用しました。

app.doScript (main, ScriptLanguage.JAVASCRIPT, [], UndoModes.ENTIRE_SCRIPT);

最後に

デジタル・アド・サービス様に納品したInDesign用スクリプトについてご紹介しました。
快く実績紹介に応じてくださったデジタル・アド・サービス様、ありがとうございました。

技術検証用にReact Nativeでサンプルアプリを作った

概要

React Nativeで、Haptic FeedbackとLottieを利用したアニメーションが実現できるのか、簡単なアプリを実装して試してみた。

実現できたか

結果から書くと、動作させること自体はとても簡単だった。
まったくのReact Native初心者の私でも環境構築含め一日程度でできた。
アプリへの組み込みは開発者の実装速度に依存するところが大きいが、ちょっと頑張ればいけるという肌感。

サンプル紹介

Haptic Feedbackの実装

以下のリポジトリでサンプルアプリを公開している。
https://github.com/macneko-ayu/RNHapticFeedbackSample

READMEに書いているとおりだけど、react-native-haptic-feedbackというパッケージを導入した。

アプリを起動すると以下のように画面上にボタンがいくつか表示される。

screen

ボタンのラベルの文字列はHaptic Feedbackとして用意されている設定と同期していて、各ボタンをタップするとその設定のHaptic Feedbackが作動する。
Haptic Feedbackを実装する際はOS標準のアプリのHaptic Feedbackを参考にして、同じ世界観になるようにこの設定の中から選択する必要があると思う。

Lottieの実装

以下のリポジトリでサンプルアプリを公開している。
https://github.com/macneko-ayu/RNLottieSample

READMEに書いているとおりだけど、lottie-react-nativeというパッケージを導入した。

アプリを実行するとLottieのリポジトリにあったサンプルのアニメーションが実行される。

output

サンプルではアニメーションを読み込んでループ再生しているだけだが、実際のアプリではスクロール時に実行したり、ボタンタップ時に実行したりと、イベントに連動するように使われるので、もう少し工夫する必要があると思う。

React Nativeについて

環境構築

公式のGetting Startedの通りにやれば大体できる。
私の環境では node_modules のPATHが通っておらずコケてしまったので、以下の記事を参考にしてPATHを通した。
ReactNativeを使ってみる

iOSの場合はいつもの如くXcodeでCode Signと戦ってくれ。
Androidは実機がなくて試してない。

所感

とにかくビルドが遅い。
Swiftがとてもかわいく思えるくらい遅い。
Hot Reloadできたりするので、積極的に利用するといい。

最後に

この程度のサンプルアプリを公開することに若干の抵抗があったけど、誰かの役に立てれば幸い。