今回は前回の続きでsipsコマンドとJavaScriptで画像処理を行ってみます。前回は簡単な四角形だけを描画しました。今回は直線で図形を描画したり画像を処理してみましょう。Canvasは高機能なので一部の機能だけ説明しています。
今回動かすJavaScriptプログラムはデスクトップ上にあるsampleディレクトリ内に入れてありますが、他のディレクトリでも問題ありません。なお、sipsコマンドの都合により実行後に保存されるファイルはカレントディレクトリになります。

直線の描画

 Canvasでの図形描画の基本中の基本が直線です。Canvasで複雑な図形を描画する場合、直線やベジエ曲線などを組み合わせます。図形はパスとして処理され、そのパスに対して線を描画したり塗りを指定することになります。

手順としては以下のようになります。

(1) beginPath()でパスを新規に生成します。
(2) パスを構築する各種メソッドを実行。この段階ではパスは描画されません。
(3) closePath()でパスを閉じます。パスを閉じないオープンパスにしたい場合は不要です。
(4) パスを塗りつぶす場合はfill()を実行します。線を描画する場合はstroke()を実行します。

直線に限らず複雑な図形も同様の手順で描画することができます。パスを生成する場合、ペンの位置を考慮する必要があります。ペンの位置は最後に生成したパスの座標になります。タートルグラフィックスと同じです、と書いてもタートルグラフィックスのLOGO言語が古すぎて分からないかもしれません。それよりも少し新しいPostScript言語と同じ、と書いてもこちらも見えない所で多く使われてるけど、なかなかマイナーなのでわからないかもしれません。要するにパスの生成は一筆書きと同じ要領だと言った方がわかるかもしれません。

・LOGO
https://ja.wikipedia.org/wiki/LOGO

・PostScript
https://ja.wikipedia.org/wiki/PostScript

という事で、論より証拠、一本の直線を描いてみましょう。ここでは、320x240のキャンバスに座標(10,5)から座標(310,235)上記の手順に従ってコードを作成していきます。
最初にCanvasのコンテキストを取得します。その後、beginPath()でパスを新規に作成します。

const canvas = new Canvas(320, 240);
const myLine = canvas.beginPath();

次に描画の最初の位置(ペン位置)を指定します。位置を指定するにはmoveTo()を使います。最初のパラメータが横方向の座標で2番目のパラメータが縦方向の座標になります。座標(10,5)に移動させるので以下のようになります。

canvas.moveTo(10,5);

ここから直線となるパスを生成するにはlineTo()を使います。最初のパラメータが横方向の座標で2番目のパラメータが縦方向の座標になります。以下のように指定すると現在のペン位置から座標(310,235)に直線のパスが生成されます。生成後は座標(310,235)が新たなペン位置になります。

canvas.lineTo(310,235);

ここまでの段階では、まだ直線のパスが生成されただけでCanvasには何も描画されません。線を描画するにはstroke()を使います。以下のようにすることで、線がCanvasに描画されます。なお、描画しても生成したパスの情報は残ったままになります。パスの情報が、残ってるので続けて塗りつぶしたりする事ができます。

const canvas = new Canvas(320, 240);
const myLine = canvas.beginPath();
canvas.moveTo(10,5);
canvas.lineTo(310,235);
canvas.stroke();
let output = new Output(canvas, "img1.png");
output.addToQueue();

Canvasのデフォルト値(初期値)では、線の色は黒、線の幅は1になっています。これらの値は変更する事ができます。
線幅はlineWidthのプロパティに整数値を指定します。以下のようにすると8ピクセルのサイズで線が描画されます。

const canvas = new Canvas(320, 240);
const myLine = canvas.beginPath();
canvas.moveTo(10,5);
canvas.lineTo(310,235);
canvas.lineWidth = 8;
canvas.stroke();
let output = new Output(canvas, "img2.png");
output.addToQueue();

線の色を指定するにはstrokeStyleプロパティにカラー値を文字列として設定します。WebでのCanvasと違い指定方法に制限があるようで、カラー名や16進数での指定ではうまくいきません。ですので以下のように文字列でrgb()を使います。rgb()内の値は赤、緑、青の順番に指定します。値は0〜255までの範囲で、値が大きくなるほど明るくなります。以下のように指定すると、一番明るい緑色で線が描画されます。

const canvas = new Canvas(320, 240);
const myLine = canvas.beginPath();
canvas.moveTo(10,5);
canvas.lineTo(310,235);
canvas.lineWidth = 8;
canvas.strokeStyle = "rgb(0,255,0)";
canvas.stroke();
let output = new Output(canvas, "img3.png");
output.addToQueue();

ここまでは一本の直線でしたが、連続して直線を描画する場合はlineTo()で座標を指定していきます。

const canvas = new Canvas(320, 240);
const myLine = canvas.beginPath();
canvas.moveTo(10,5);
canvas.lineTo(310,235);
canvas.lineTo(160,235);
canvas.lineTo(10,80);
canvas.lineWidth = 8;
canvas.strokeStyle = "rgb(0,255,0)";
canvas.stroke();
let output = new Output(canvas, "img4.png");
output.addToQueue();

 lineTo()で連続で直線を描くことができますが、その際最初の座標点と最終座標を結ぶかどうかを決める事ができます。
 パスが閉じていない場合はオープンパス、閉じている場合はクローズパスとなります。パスを閉じる場合はclosePath()を使います。先ほどの連続直線でパスを閉じると最初と最後の座標が結ばれて、描かれる図形が変わります。

const canvas = new Canvas(320, 240);
const myLine = canvas.beginPath();
canvas.moveTo(10,5);
canvas.lineTo(310,235);
canvas.lineTo(160,235);
canvas.lineTo(10,80);
canvas.closePath();
canvas.lineWidth = 8;
canvas.strokeStyle = "rgb(0,255,0)";
canvas.stroke();
let output = new Output(canvas, "img5.png");
output.addToQueue();

図形を塗り潰すには前回も使ったfill()を使います。以下のようにすると図形が緑色で塗り潰されます。

const canvas = new Canvas(320, 240);
const myLine = canvas.beginPath();
canvas.moveTo(10,5);
canvas.lineTo(310,235);
canvas.lineTo(160,235);
canvas.lineTo(10,80);
canvas.closePath();
canvas.lineWidth = 8;
canvas.fillStyle = "rgb(0,255,0)";
canvas.fill();
let output = new Output(canvas, "img6.png");
output.addToQueue();

図形を塗りつぶして、さらに線を描画する場合は以下のようにすれば動きそうに思えます。しかし、sipsでの描画結果は一般的なCanvasでの描画とは異なります。実行すると塗りつぶしは行われるものの線画が描画されません。塗りつぶすか線を描画するとパス情報が消去されてしまうようです。

const canvas = new Canvas(320, 240);
const myLine = canvas.beginPath();
canvas.moveTo(10,5);
canvas.lineTo(310,235);
canvas.lineTo(160,235);
canvas.lineTo(10,80);
canvas.closePath();
canvas.lineWidth = 8;
canvas.fillStyle = "rgb(0,255,0)";
canvas.fill();
canvas.strokeStyle = "rgb(255,0,0)";
canvas.stroke();
let output = new Output(canvas, "img7.png");
output.addToQueue();

ちなみにWebブラウザで同じコードを実行すると期待通りに赤色の線が描画されます。

<!DOCTYPE html>
<html><head><title>sample7</title></head>
<body>
<canvas width="320" height="240" id="myCanvas"></canvas>
<script>
const canvas = document.getElementById("myCanvas").getContext("2d");
const myLine = canvas.beginPath();
canvas.moveTo(10,5);
canvas.lineTo(310,235);
canvas.lineTo(160,235);
canvas.lineTo(10,80);
canvas.closePath();
canvas.lineWidth = 8;
canvas.fillStyle = "rgb(0,255,0)";
canvas.fill();
canvas.strokeStyle = "rgb(255,0,0)";
canvas.stroke();
</script>
</body>
</html>

sipsの場合、以下のように、それぞれにパスを構築しないと塗りと線が描画されないようです。

const canvas = new Canvas(320, 240);
let myLine = canvas.beginPath();
canvas.moveTo(10,5);
canvas.lineTo(310,235);
canvas.lineTo(160,235);
canvas.lineTo(10,80);
canvas.closePath();
canvas.lineWidth = 8;
canvas.fillStyle = "rgb(0,255,0)";
canvas.fill();
myLine = canvas.beginPath();
canvas.moveTo(10,5);
canvas.lineTo(310,235);
canvas.lineTo(160,235);
canvas.lineTo(10,80);
canvas.closePath();
canvas.strokeStyle = "rgb(255,0,0)";
canvas.stroke();
let output = new Output(canvas, "img8.png");
output.addToQueue();

 直線が描ければ計算によって、どんな図形も描くことができます。が、それは面倒なので使用頻度の高い図形に関しては描画するためのメソッドがいくつも用意されています。詳しくはCanvas 2Dに関するページ等を参照してください。

・CanvasRenderingContext2D
https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D

画像の読み込みと描画

 次に画像を読み込んで配置してみましょう。読み込む画像はスクリプトを実行する際に画像ファイルがある場所、つまりファイルパスを指定します。また、指定できる画像ファイルはひとつだけでなく半角空白などで区切って複数指定することができます。

 まず、スクリプトファイルと同じディレクトリにある画像ファイル(flower1.jpg)をひとつだけ指定して描画させてみましょう。

なお、スクリプトファイルと画像ファイルがあるディレクトリがカレントディレクトリです。 画像を描画するにはdrawImage()を使います。最初のパラメーターにsipsコマンドで指定した画像ファイル情報を指定します。最初に指定した画像はsips.images[0]として参照・指定できます。2番目に指定した画像はsips.images[1]となります。指定された画像をまとめて描画する場合はforループまたはforEach()などを使って処理します。
drawImage()の2番目のパラメーターはCanvasに描画する画像のX座標になります。3番目のパラメーターはCanvasに描画する画像のY座標になります。

const canvas = new Canvas(320, 240);
canvas.drawImage(sips.images[0],20,10);
let output = new Output(canvas, "img9.png");
output.addToQueue();

読み込んだ画像に対してピクセル単位の処理もできます。手順や方法としては一般的なCanvasと同じです。まず、getImageData()でCanvasで取得したい画像の範囲を指定します。getImageData()の最初のパラメーターがX座標、2番目のパラメーターがY座標、3番目のパラメーターが横幅、4番目のパラメーターが縦幅になります。戻り値はImageDataオブジェクトで、取得したピクセルデータはImageDataオブジェクトのdata配列に入ります。この配列は1ピクセルが4要素で構成されています。

+0  赤色の輝度(0〜255)
+1  緑色の輝度(0〜255)
+2  青色の輝度(0〜255)
+3  不透明度(0〜255)

なお、data配列内のピクセルデータを変更してもCanvas画像には反映されません。処理した結果を反映させるにはputImageData()を使います。最初のパラメーターにピクセルデータを処理したImageDataオブジェクトを指定します。2番目のパラメーターにはCanvasに描画するX座標、3番目のパラメーターにはY座標を指定します。

以下のプログラムを実行すると読み込まれた画像の緑成分の情報をカットした結果を画像ファイルとして保存します。

const canvas = new Canvas(320, 240);
canvas.drawImage(sips.images[0],0,0);
let myImageData = canvas.getImageData(0,0,320,240);
let pixel= myImageData.data;
for(let i=0; i<pixel.length; i+=4){
 pixel[i]=pixel[i]; // Red
 pixel[i+1]=0;  // Green
 pixel[i+2]=pixel[i+2]; // Blue
}
canvas.putImageData(myImageData,0,0);
let output = new Output(canvas, "img10.png");
output.addToQueue();

このようにsipsを使えばJavaScriptで手軽に画像加工・画像処理ができます。

テキストの描画

 sipsではテキストの描画もできます。テキストの描画はfillText()メソッドを使います。最初に描画する文字列、2番目のパラメーターにX座標、3番目のパラメーターにY座標を指定します。4番目のパラメーターは横の最大幅になります。4番目のパラメーターは省略する事ができます。表示する文字のサイズや書体はfontプロパティに指定します。 以下のようにすると座標系(30,20)にMyNaviの文字が赤色で描画されます。

const canvas = new Canvas(320, 240);
canvas.fillStyle = "rgb(255,0,0)";
canvas.font="48pt Times";
canvas.fillText("MyNavi",30,100);
let output = new Output(canvas, "img11.png");
output.addToQueue();

線だけの文字、つまりアウトライン文字(袋文字)も描画できます。この場合、strokeText()メソッドを使います。指定できるパラメーターはfillText()とおなしです。 以下のようにすると、座標(30,20)にアウトライン文字が描画されます。

const canvas = new Canvas(320, 240);
canvas.strokeStyle = "rgb(255,0,0)";
canvas.font="48pt Times";
canvas.strokeText("MyNavi",30,100);
let output = new Output(canvas, "img12.png");
output.addToQueue();

sipsを使えば新たにアプリケーションなどをインストールすることなくJavaScriptでいろいろなことができます。標準でできると言うのは意外に強力なものです。sipsコマンドは使い方次第では多大な力を発揮するかもしれません。

著者 仲村次郎
いろいろな事に手を出してみたものの結局身につかず、とりあえず目的の事ができればいいんじゃないかみたいな感じで生きております。