進化した macOS の sips コマンド + JavaScript でお絵描きする

## `sips` コマンドとは

macOS には標準で `/usr/bin/sips` に画像処理用のコマンドがあります。
ImageMagick などに頼らなくても簡単な画像処理ができるツールとして重宝されています。

従来の `sips` コマンドの使い方については Qiita にもいくつか記事が上がっています。

- [Macのターミナルで簡単に画像処理できるsipsの使い方](https://qiita.com/livlea/items/53b755e5067d4ebc5b43)
- [macOS の標準コマンドで Jpeg 画像を PNG に変換するワンライナー](https://qiita.com/KEINOS/items/8bbf7217904ea832e1b4)

最近 (おそらく Big Sur 以降 [^1]) になって、`sips` コマンドがこっそり強化され、JavaScript を使って画像の操作が行えるようになりました。

インターフェースとしては `<canvas>` 要素のものを模倣しており、内部的には CoreGraphics を使って実装されているようです。
これによって従来 `sips` ではできなかった、柔軟な画像処理ができるようになってきました。


実装としては JavaScript の実行環境は JavaScriptCore.framework を使っていて、 Canvas APIは CoreGraphics.framework で同等のものを実装して、JavaScript とブリッジしているようです。

## JavaScript インターフェースの使い方

さっそく `sips` の manpage を見てみましょう。

```sh
$ man sips
...
     -j file
     --js file
	   Execute JavaScript file
...
JavaScript
     HTML Canvas objects can be created and used to create a 2D drawing context.  The com-
     mands for drawing into the context are well documented elsewhere.	This section will
     describe the sips global object and other interesting classes.
...
```

`-j` `--js` オプションに JavaScript ファイルを渡すことで、画像の操作ができそうです。
また、HTML の `<canvas>` で使える操作ができるとのことですが、その詳細はドキュメントには書かれていません。


## Hello world

というわけで早速 Hello world してみましょう。

```js:hello.js
console.log('Hello, world!');
```

```sh
$ sips -j hello.js
Hello, world!
```

はい、無事 JavaScript が実行されています。
`console.log` 以外に `print` 関数も定義されているので、書き換えても動作します。

また、JavaScriptCore では shebang が使えるのでスクリプト単体を配布することもできます。

```js:hello.js
#!/usr/bin/sips -j

console.log('Hello, world!');
```

このファイルに実行属性をつければ、以下のようにスクリプト単体で実行できます。

```sh
$ ./hello.js
Hello, world!
```

## ゼロから画像を生成

`<canvas>` API を使ってゼロから画像を生成することもできます。

試しに MDN の Canvas API Tutorial のページに載っているサンプルを描いてみましょう。

https://developer.mozilla.org/ja/docs/Web/API/Canvas_API/Tutorial/Basic_usage

```js:rect.js
const canvas = new Canvas(150, 150);
canvas.fillStyle = 'rgb(200,0,0)';
canvas.fillRect(10, 10, 50, 50);
canvas.fillStyle = 'rgba(0,0,200,0.5)';
canvas.fillRect(30, 30, 50, 50);
const output = new Output(canvas, 'rect.png');
output.addToQueue();
```

```sh
$ sips -j rect.js
```

`rect.png`:
![rect.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73424/93b14102-58b8-053e-50c4-6d6619f30a57.png)

はい、無事2つの四角形が描画されました。
MDN のサイトに載っている Canvas API を使ったサンプルと異なるところがいくつかあるので見ていきましょう。

まず最初は `Canvas` オブジェクトの生成です。
通常ブラウザの API では HTML で `<canvas>` 要素を定義しておいて、`document.getElementById()` などを呼んで `HTMLCanvasElement` を取得しますが、`sips` コマンドの API では直接 `new Canvas(width, height)` の形でインスタンス化できます。

次に、 `canvas.getContext('2d')` を読んで 2D 描画コンテキストを取得する必要がない点です。
この API は定義自体はされているので、呼んでも構いませんが、実装としては単に `this` を返すだけになっているようです。したがって Canvas API の `CanvasRenderingContext2D` オブジェクトに対する操作は `Canvas` オブジェクト自体に対してすることができます。

最後に、描画した画像を書き出す API が存在していることです。

`Output` オブジェクトは `new Output(context, name, type)` の形でインスタンス化できます。
`context` 引数には `Canvas` オブジェクト、`name` 引数には出力先のパス、省略可能な `type` 引数には出力ファイルの拡張子か UTI を渡します。
`type` 引数が省略された場合は `name` 引数の拡張子から判断されるようです。

[UTI](https://developer.apple.com/documentation/uniformtypeidentifiers/) の概念は macOS 独特のもので馴染みがない方もいると思うので、利用可能な拡張子と UTI を載せておきます。

拡張子|UTI
-|-
png|public.png
jpeg / jpg|public.jpeg
gif|com.compuserve.gif
tiff|public.tiff
bmp|com.microsoft.bmp

## パスを使った例

Canvas API と同じく、パスを使った描画も可能です。
やはり MDN のサンプルがほぼ変更なしに動作します。

https://developer.mozilla.org/ja/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes

```js:smile.js
const canvas = new Canvas(150, 150);
canvas.beginPath();
canvas.arc(75, 75, 50, 0, Math.PI * 2, true);
canvas.moveTo(110, 75);
canvas.arc(75, 75, 35, 0, Math.PI, false);
canvas.moveTo(65, 65);
canvas.arc(60, 65, 5, 0, Math.PI * 2, true);
canvas.moveTo(95, 65);
canvas.arc(90, 65, 5, 0, Math.PI * 2, true);
canvas.stroke();
const output = new Output(canvas, 'smile.png');
output.addToQueue();
```

```sh
$ sips -j smile.js
```

`smile.png`

![smile.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73424/a1654a13-7c50-236d-5038-20d53b370f41.png)

## 画像を使った例

別の画像を読み込むこともできます。
先程までの例で生成された画像を一枚の画像にしてみましょう。

```js:composite.js
const canvas = new Canvas(150, 150);
sips.images.forEach(image => {
  canvas.drawImage(image, 0, 0);
});
const output = new Output(canvas, 'composite.png');
output.addToQueue();
```

```sh
$ sips -j composite.js rect.png smile.png
```

`composite.png`

![composite.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73424/416cb2aa-b1bc-025a-c8bc-2fdf8db78706.png)

## テキストを描画する

`sips` コマンドには CoreText.framework を使ったテキスト描画の機能も Canvas API の一部として提供されています。

```js:text.js
const canvas = new Canvas(100, 100);
canvas.filllStyle = 'black';
canvas.fillRect(0, 0, canvas.width, canvas.height);
canvas.font = '42pt Futura';
canvas.textAlign = 'center';
canvas.textBaseline = 'middle';
canvas.fillStyle = 'skyblue';
canvas.fillText('TEXT', canvas.width / 2, canvas.height / 2);
const output = new Output(canvas, 'text.png');
output.addToQueue();
```

```sh
$ sips -j text.js
```

![text.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73424/5f228393-f14c-343a-4c71-2a4c3d9ce6be.png)

## 実装されていない機能

ここまで紹介した機能で十分高度な画像編集ができそうですが、一部 Canvas API に比べて動作が異なる機能があります。

例えば、Canvas API の `CanvasRenderingContext2D.createPattern()` は `sips` だと `Canvas.createPattern()` にあたり、ここまでは実装されているものの、作成したオブジェクトを利用する API が実装されていないようです。
通常は `canvas.fillStyle` などに渡すのですが、`canvas.fillStyle = canvas.createPattern(...)` では渡せず、`sips` バイナリの中にもそのような処理は実装されていないように見えます。

## まとめ

これまで ImageMagick などで行っていた画像処理が macOS 標準コマンドでできるようになりました。
API もブラウザの API をよく再現していて知識を使いまわせるので、これから使っていくと良さそうです。

## おまけ : 非公式 API リファレンス

公式の manpage にも簡単な使い方が載っていますが、網羅的ではないため [Hopper Disassember](https://www.hopperapp.com/) を使って `sips` コマンドの中で実装されている API を解析してみました。
よく使いそうな API は実際に試して動作を確認し、[typedoc](https://typedoc.org/) でドキュメントを作成しました。
以下に置いておいたので、もしよければ参考にしてください。


https://manicmaniac.github.io/sips-js-api/ 


[^1]: Catalina でも manpages に `-j` `--js` オプションの記載はあり一見できそうですが、クラッシュしてしまい動作を確認できなかったためバグレポートを起票しています https://openradar.appspot.com/radar?id=5061043655016448