くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

Cropper.jsで画像切り抜きWebアプリを作ってみる

画像の一部を簡単に切り抜きたいなと思って、いろいろ調べてみたら、
Cropper.jsという便利なライブラリがあったのでお試し(´ω`)

こんなのを作りました(´ω`)

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」に書いてあった。

image

getContainerData()とかgetCropBoxData()とか、
いろいろあるので見ておくとよかった。

dragMode

Cropper.jsには、「crop」と「move」という2つのモードがある。

dragMode | Options

crop」では、ドラッグでcrop boxを作成できる。 「move」では、ドラッグでcanvasを移動できる。

マウスホイールで拡大縮小できるけど、「画像の表示部分を変えたいな〜」と思ったら、
move」モードにして、画像の表示位置を移動する感じ。

設定関連

containerStyle

Containerのdivのstyle。vue-cropperjs固有の設定

<vue-cropper :containerStyle="containerStyle" />

vue-cropperjs: GitHub

imgStyle

imgタグのstyle。vue-cropperjs固有の設定

<vue-cropper :imgStyle="imgStyle" />

vue-cropperjs: GitHub

alt

imgタグのalt。vue-cropperjs固有の設定

<vue-cropper :alt="alt" />

vue-cropperjs: GitHub

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"
/>

ready | Events | Cropper.js

crop

canvasかcrop boxが変更されると呼ばれるイベント。
crop boxのサイズを取得するときに使った。

<vue-cropper
  @crop="onChangeCrop"
/>

crop | Events | Cropper.js

ハマったポイント

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つ。

  1. top/leftはcontainerを基準にしている
  2. 上記と同じで、オリジナルと表示時の画像サイズ比率の考慮が必要

なので、中央に配置したい場合は、こんな感じになった。

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);

以上!!

参考にしたサイト様