画像の一部を簡単に切り抜きたいなと思って、いろいろ調べてみたら、
Cropper.jsという便利なライブラリがあったのでお試し(´ω`)
こんなのを作りました(´ω`)
いろいろ遊んでたらなんかできた(*´ω`*)https://t.co/FPc1ylaVqH pic.twitter.com/KdDLeXiY1q
— きらぷか📚積読ハウマッチ/カブれこをリリース🎉 (@kira_puka) April 21, 2021
Webアプリ自体は、Nuxtで作っているので、
vue-cropperjsというVueのラッパーライブラリも利用。
ただ、Cropper.js自体がいろいろでき、理解するのが大変だったので、つまったところを残しておく。。(´ω`)
Cropper.jsのデモもある。
・Demo | Cropper.js
まずはインストール
vue-cropperjsでは、Vue3.xに対応しているけど、
Vue2を使っているので、バージョン4.2.0を指定してインストール
$ npm i vue-cropperjs@4.2.0
とりあえず表示してみる
一番基礎的なのはこんな感じ。
VueCropperコンポーネントに表示したい画像をsrc
に設定すればOK。
cropperには、refsを使ってアクセスする感じ。
<template> <div> <!-- vue-cropperを使った切り抜き画面 --> <client-only> <div class="preview"> <vue-cropper ref="cropper" :src="preview" /> </div> </client-only> </div> </template> <script lang="ts"> import { Component, Watch, Vue } from "nuxt-property-decorator"; // vue-cropperjsで必要なimport import "cropperjs/dist/cropper.css"; import VueCropper from "vue-cropperjs"; @Component({ components: { VueCropper } }) export default class ClipImagePage extends Vue { // **************************************************** // * computed // **************************************************** private get preview() { return "/default_image.png"; } private get cropper() { return this.$refs.cropper as any; } } </script>
選択したファイルを表示する
こんな感じ。Buefyを使ってます。
選択されたファイルをFileReaderで開いて、
取得したDataURLをVueCropperコンポーネントのsrc
に設定して、
this.cropper.replace()
を呼び出せばOK
<template> <div> <!-- ファイルアップロードボタン --> <div> <b-field class="file is-primary"> <b-upload v-model="file" class="file-label" accept="image/*"> <span class="file-cta"> <span class="file-label">Select Image</span> </span> </b-upload> </b-field> </div> <!-- vue-cropperを使った切り抜き画面 --> <!-- 略 --> </div> </template> <script lang="ts"> import { Component, Watch, Vue } from "nuxt-property-decorator"; // vue-cropperjsで必要なimport import "cropperjs/dist/cropper.css"; import VueCropper from "vue-cropperjs"; @Component({ components: { VueCropper } }) export default class ClipImagePage extends Vue { private file: File | null = null; private fileData: string | null = null; // **************************************************** // * computed // **************************************************** private get preview() { return this.fileData || "/default_image.png"; } private get cropper() { return this.$refs.cropper as any; } // **************************************************** // * watch // **************************************************** @Watch("file") private setImage(target: File | null) { if (!target) return; const reader = new FileReader(); reader.onload = (e) => { if (!e.target) return; this.isReady = false; this.fileData = e.target.result as string; this.cropper.replace(e.target.result); }; reader.readAsDataURL(target); } } </script>
選択した領域を画像としてダウンロードする
ファイルの保存には、便利なライブラリFileSaver.jsを使う。
$ npm i file-saver
$ npm i -D @types/file-saver
保存処理としては、こんな感じ。
private save() { // cropperから切り抜き部分のCanvasを取得 const canvas = this.cropper.getCroppedCanvas() as HTMLCanvasElement; // CanvasからBlobを取得 canvas.toBlob((blob) => { if (blob == null) return; // saveAsでBlobをダウンロード saveAs(blob, "clipImage.png"); }); }
全体はこんな感じ
<template> <div> <!-- ファイルアップロードボタン --> <!-- 略 --> <!-- ダウンロードボタン --> <div v-if="!!preview"> <b-button type="is-primary" @click="save" rounded>DOWNLOAD</b-button> </div> <!-- vue-cropperを使った切り抜き画面 --> <!-- 略 --> </div> </template> <script lang="ts"> import { Component, Watch, Vue } from "nuxt-property-decorator"; // vue-cropperjsで必要なimport import "cropperjs/dist/cropper.css"; import VueCropper from "vue-cropperjs"; // ファイルのダウンロードライブラリFileSaver import { saveAs } from "file-saver"; @Component({ components: { VueCropper } }) export default class ClipImagePage extends Vue { private file: File | null = null; private fileData: string | null = null; // **************************************************** // * computed // **************************************************** /// ...略 // **************************************************** // * watch // **************************************************** /// ...略 // **************************************************** // * methods // **************************************************** private save() { const canvas = this.cropper.getCroppedCanvas() as HTMLCanvasElement; canvas.toBlob((blob) => { if (blob == null) return; saveAs(blob, "clipImage.png"); }); } } </script>
その他、Cropper.jsの使い方
調べた・使ったものだけ。
Cropper.jsの基本概念
container/canvas/image/cropbox
「getContainerData() | Methods | GitHub」に書いてあった。
getContainerData()
とかgetCropBoxData()
とか、
いろいろあるので見ておくとよかった。
dragMode
Cropper.jsには、「crop」と「move」という2つのモードがある。
「crop」では、ドラッグでcrop boxを作成できる。 「move」では、ドラッグでcanvasを移動できる。
マウスホイールで拡大縮小できるけど、「画像の表示部分を変えたいな〜」と思ったら、
「move」モードにして、画像の表示位置を移動する感じ。
設定関連
containerStyle
Containerのdivのstyle。vue-cropperjs固有の設定
<vue-cropper :containerStyle="containerStyle" />
imgStyle
imgタグのstyle。vue-cropperjs固有の設定
<vue-cropper :imgStyle="imgStyle" />
alt
imgタグのalt。vue-cropperjs固有の設定
<vue-cropper :alt="alt" />
dragMode
dragModeの設定。Cropper.jsの設定値。
<vue-cropper dragMode="move" /> <vue-cropper dragMode="crop" /> <vue-cropper dragMode="none" />
・dragMode | Options | Cropper.js
toggleDragModeOnDblclick
ダブルクリックでモードを変更できるかのフラグ。Cropper.jsの設定値。
<vue-cropper :toggleDragModeOnDblclick="false" />
・toggleDragModeOnDblclick | Options | Cropper.js
event
vue-cropperを使うと、Cropper.jsのイベントはv-on
で受け取れる。
ready
画像が読み込まれてcropperインスタンスがreadyになったら呼ばれるイベント。
crob boxの初期サイズを設定するときに使った。
<vue-cropper @ready="onReadyCrop" />
crop
canvasかcrop boxが変更されると呼ばれるイベント。
crop boxのサイズを取得するときに使った。
<vue-cropper @crop="onChangeCrop" />
ハマったポイント
vue-cropperのsrcがnullだとうまく表示されない
vue-cropper側でsrcがnullかどうかを見ていないので、
<vue-cropper ref="cropper" :src="null"/>
とすると、こんな感じのsrcがない、imgタグが生成される。。
<div> <img alt="image" style="max-width: 100%"> </div>
さらに、vue-cropper内でCropper.jsを初期化するのがmount時のみのため、
そのまま、srcに画像を設定したりしてもエラーになる。。
なので、初期データとして画像を設定しておくのがいい感じだった。。
(こういうことをしたい場合は、vue-cropperを使わず、Cropper.jsを直接のほうがいいかも)
crop boxのwidth/heightが正しく設定されない
最初はこんな感じで、フォームで入力したwidht/heightを設定。
でも、意図した値になってなく。。
const data = Object.assign({}, this.cropper.getCropBoxData(), { width: this.areaWidth, height: this.areaHeight, }); this.cropper.setCropBoxData(data);
Stack Overflowに解決策があった。
Croper.js内で、オリジナルの画像サイズと表示してる画像サイズの比率を考慮してるらしい。。
参考にしたのは、こんな感じ。setCropBoxData()
で設定するときも比率を考慮して設定。
const imageData = this.cropper.getImageData(); const ratio = imageData.width / imageData.naturalWidth; const data = Object.assign({}, this.cropper.getCropBoxData(), { width: this.areaWidth * ratio, height: this.areaHeight * ratio, }); this.cropper.setCropBoxData(data);
crop boxのtop/leftが正しく設定されない
setCropBoxData()
でtop/leftを設定するのに少し悩んだ。。
原因は2つ。
- top/leftはcontainerを基準にしている
- 上記と同じで、オリジナルと表示時の画像サイズ比率の考慮が必要
なので、中央に配置したい場合は、こんな感じになった。
const imageData = this.cropper.getImageData(); const ratio = imageData.width / imageData.naturalWidth; const containerData = this.cropper.getContainerData(); const top = (containerData.height / ratio - this.areaHeight) / 2; const left = (containerData.width / ratio - this.areaWidth) / 2; const data = Object.assign({}, this.cropper.getCropBoxData(), { top: top * ratio, left: left * ratio, width: this.areaWidth * ratio, height: this.areaHeight * ratio, }); this.cropper.setCropBoxData(data);
以上!!