散布図や折れ線グラフなどの画像しかない時に、そのグラフの値を数値化(デジタイズ)したい場合がある。大抵の場合、「その画像を読み込む→座標軸を合わせる→グラフの線などに沿って自分で点をプロットする」といった手順を踏んで数値化していくことになる。このような数値化処理を実現するものをデジタイザと呼ぶ。今回はAngularとPaper.jsを利用してグラフのデジタイザを作ることを最終目標とし、まず最初の一歩として画像を読み込んでキャンバスに表示する機能の実装についてまとめる。
開発環境
- Angular 10.1.2
- Angular CLI 10.1.2
- Angular CDK 10.2.4
- typescript 4.0.3
- Node 14.11.0
- paper 0.12.11
- bootstrap
完成イメージ
- 読み込みボタンをクリックしてファイル選択ウインドウを開く
- 画像を読み込む
ディレクトリ構成
src/app/
以下のディレクトリ構成は下記の通り。今回の説明で主に登場するのはdigitizer.component.html
、digitizer.component.ts
になる。
app ├── app-routing.module.ts ├── app.component.html ├── app.component.scss ├── app.component.ts ├── app.module.ts ├── components │ └── digitizer │ ├── digitizer.component.html │ ├── digitizer.component.scss │ └── digitizer.component.ts └── models
実装
画像を読み込む
画像を表示するためのcanvas
要素、画像読み込み用のbutton
とinput
要素、画像保持用のimg
要素が主な部品になるのでHTML側に記述していく。
まず、画像読み込みボタンから間接的に操作するinput
要素は非表示にしておく。本来は読み込み可能な画像ファイルの種類を指定すべきであるが今回は特に気にせずaccept="image/*"
にしている。
また、実際に画像を表示するのはcanvas
要素なのでimg
要素も非表示にしておく。キャンバスに画像を表示するためには、予め画像を読み込んでimg
要素にセットしておく必要があるのでこのような形になる。
さらに、クラスファイルの方で@ViewChild
を使ってcanvas
要素とinput
要素を取得したいので、参照変数としてそれぞれ#canvas
と#fileInput
をつけておく。
<div class="flame d-flex flex-column mt-4"> <div class="d-flex"> <div class="col-6 canvas-wrapper ml-2"> <div> <canvas class="m-0" #canvas></canvas> </div> <div class="d-flex image-configure mt-2"> <div class="col-5 p-0"> <button mat-raised-button (click)="onClickFileInputButton()">画像を読み込む</button> </div> </div> </div> </div> </div> <!-- 画像読込み用 --> <input type="file" style="display: none;" #fileInput accept="image/*" (change)="onChangeFileInput($event)"> <img id="image" style="display: none;" src="{{imgSrc}}">
一応スタイルファイルも。SCSSを利用している。
.flame { width: 1450px; overflow: auto; } .canvas-wrapper { canvas { border: solid 1px #979797; width: 700px; height: 500px; &:hover { cursor: crosshair; } } .outrange { color: #ff0000; } } .mat-raised-button { color: #ffffff; background-color: #299c33; outline: none; }
次にコンポーネントのクラスファイルを記述していく。まずはじめに@ViewChild
でcanvas
要素とinput
要素のDOMを取得する。
画像を読み込む
ボタンをクリックした時に呼び出されるメソッド。
this.fileInput.nativeElement.click()
でinput
要素をクリックするだけなのだが、既に画像が表示されている状態で再度画像ファイルを読み込もうとした時に上書きして良いかどうかを確認するメッセージを表示するようにした。
input
要素が変更された時に呼び出されるメソッド。
まず、new Image()
でHTMLImageElement
オブジェクトを生成しimage
に格納する。また、画像が読み込まれた時点で発火するonload
のコールバック関数を設定しておく。(現時点では中身は空だが後でキャンバスに画像を表示する処理を記述していく)
次に、new FileReader()
で生成されるFileReader
オブジェクトをfileReader
に格納し、$event
として渡されたイベントオブジェクトに含まれるファイル情報をfile
に格納する。
その後、fileReader
のreadAsDataURL()
でファイルオブジェクトfile
の読み込みを開始する。読み込み完了後に発火するfileReader.onload
のコールバック関数内でimgSrc
( img
タグのsrc
属性)とimage.src
(Image
オブジェクトのsrc
)にそれぞれfileReader.result
を格納する。ただし、image.src
はstring
型である必要がありstring | ArrayBuffer
型のfileReader.result
を直接格納することができないためString
型でキャストしてやる必要がある。image.src
にfileReader.result
が格納された時点で画像の読み込みが開始され、完了すると前述のimage.onload
が発火する。
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import * as paper from 'paper'; import { Raster } from 'paper'; import { view } from 'paper/dist/paper-full'; @Component({ selector: 'app-digitizer', templateUrl: './digitizer.component.html', styleUrls: ['./digitizer.component.scss'] }) export class DigitizerComponent implements OnInit { @ViewChild('canvas', { static: true }) canvas: ElementRef<HTMLCanvasElement>; @ViewChild('fileInput', { static: false }) fileInput: ElementRef; file: File = null; imgSrc: string | ArrayBuffer = ''; constructor() { } ngOnInit(): void { paper.setup(this.canvas.nativeElement); } onChangeFileInput(event): void { if (event.target.files.length === 0) { this.file = null; this.imgSrc = ''; return; } const image = new Image(); image.onload = () => { // キャンバスに画像を表示する処理(後述) }; const fileReader = new FileReader(); this.file = event.target.files[0]; fileReader.readAsDataURL(this.file); fileReader.onload = () => { this.imgSrc = fileReader.result; image.src = fileReader.result as string; }; } onClickFileInputButton(): void { if (!this.file || confirm('新しく画像を読み込みますか?\n前の画像やプロット状態などは保存されません。')) { this.fileInput.nativeElement.click(); } } }
キャンバスに画像を表示する
Image
オブジェクトの読み込みが完了した後に発火するimage.onload
のコールバック関数でsetImageToCanvas()
を呼び出す。
画像表示のためのメソッド。
Paper.jsでは、画像はRaster
オブジェクトとして扱っていく。new Raster('image')
でRaster
オブジェクトを新規作成することができ、引数には表示対象の画像データを保持しているimg
要素のid
(今回は'image'
)を指定する。
さらに、初期はキャンバスの中央に画像を表示したいので、生成したRaster
オブジェクトの中心位置position
をキャンバスの中心位置に合わせる。view.center
はキャンバス(view)の中心座標のPoint
オブジェクトになるのでそれを指定すればよい。
onChangeFileInput(event): void { // 省略 const image = new Image(); image.onload = () => { this.setImageToCanvas(); // 追加 }; // 省略 } // 省略 private setImageToCanvas(): void { // キャンバス上のオブジェクトを全てクリア paper.project.activeLayer.removeChildren(); const raster = new Raster('image'); // Rasterオブジェクトの中心をキャンバスの中心に合わせる raster.position = view.center; }
以上で、読み込んだ画像をキャンバスに表示することができる。
おわりに
今回はキャンバスに画像を表示するための基本的なことを書いた。次の記事では、読み込んだ画像の平行移動(パン)と拡大縮小(ズーム)機能について述べる。最初はこの記事の中で画像読み込み・表示と一緒にまとめてしまおうかと思ったが、意外と内容が重かったので分けることにした。
参考