【Flutter】画像切り抜きパッケージ crop_your_image を作っています

crop_your_image という __Flutter 用の画像切り抜きパッケージ__を作っています。

https://pub.dev/packages/crop_your_image

もともとは「Flutter で使える画像切り抜きパッケージでいい感じのあんまり無いよねー」という雑談をきっかけにざっと試しで作ってみたというものなのですが、いろいろ手を加えるうちに ver 0.1.0 で少しだけ使える状態にまでなったので、この記事で紹介したいと思います。

![qiita_demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134943/350e4807-dffd-a861-c017-6b5744f6c68a.gif)

# モチベーション

ご存知の通り、 Flutter にはすでにいくつかの画像切り抜きパッケージが公開されています。例えば、

https://pub.dev/packages/image_crop

https://pub.dev/packages/image_cropper

このあたりはプロフィール画像設定のような機能で画像切り抜きがしたくなった時に一度は検討したパッケージなのではないでしょうか。自分もその1人です。

確かにこれらのパッケージは使いやすく簡単に画像切り抜き機能が実現できる一方、以下のようにいくつか気になる点が存在します。

- **UI が固定である**

切り抜き操作の UI はパッケージが提供するもので固定です。アプリ側から変えられるのは、それぞれの文言や色、表示非表示といった程度で、全体的にどのボタンがどこに置かれているか、どれを押したらどのアクションが実行されるか、を変えることはできません。

これは UI に拘らないプロダクトであればむしろ UI の実装コストがかからない分プラスに働きますが、逆に UI の細部にまでブランドの統一感を持たせたいプロダクトにとっては、ここだけ「違う世界感」が出てしまって、ユーザーに違和感を与える原因になりえます。

- **プラットフォームが限られている**

いくつかのパッケージは切り抜き処理をネイティブ（Swift / Kotlin）で行っているため、そのまま Web や Mac, Linux 向けのアプリケーションで使うことができなかったりします。 

Flutter 2.0 によって Flutter のマルチプラットフォーム開発がさらに加速することを考えると、 Android / iOS のみでしか動作しないことは足枷になるリスクがあります。

- **その他**

パッケージによりさまざまですが、開発がストップしていて Null Safety 対応がされていない、 README があまり書かれていない、逆に多機能すぎて学習コストが高い、など、どのパッケージも「完璧な」ものでない状況が見えてきます。

以上のような理由があり、自分の考える「理想な」パッケージが見つからないなと考えていたところで「じゃあ自分で作ればいいじゃん」と思いたって開発をスタートした、というのが根本のモチベーションになっています。

# ゴール

まだ全然実現できていませんが、crop_your_image パッケージの目指すゴールは大まかに以下の2点です。

- **UI の提供を最低限にする**

画像の切り抜き作業を行う UI は画像の上で範囲を絞る UI のみに限定し、切り抜きの実行、比率の指定、リセット、出力形式の指定などのアクションを行うための UI は全てアプリ開発者に委ねる設計を目指しています。

これは `video_player` パッケージが参考になっていて、ひとつひとつの操作を行うための `Controller` をアプリ側で管理し、好きなタイミングで好きな操作をその Controller 経由で呼び出すことによって自由に UI や機能を自分のアプリに組み込める、という仕組みです。

https://pub.dev/packages/video_player

- **プラットフォームに依存しない**

PlatformChannel を使わず、 Dart コードのみで実装することで Flutter フレームワーク自体が対応する通りのプラットフォームで利用できるパッケージを目指しています。

そのため、画像の切り抜き処理には純粋な Dart の画像処理ライブラリである `image` パッケージのみを利用しています。

https://pub.dev/packages/image

上記の 2 つを中心に、あとは

- 使いやすい API デザイン
- 使い方や使いどころが伝わる README.md
- テストコード
- サンプルアプリ

などを可能な限り充実していければと考えています。

また、パッケージ開発は自分のとって初めての試みであるため、そのあたりの経験と知見をためるのも大きな目的のひとつです。

# 仕組み

画像切り抜き機能というと人によっては小難しい印象を受けるかもしれませんが、このパッケージ自体がやっていることはとてもシンプルです。

## image パッケージを使って切り抜いたデータを作成する

`image` パッケージには画像を切り抜くための [copyCrop()](https://pub.dev/documentation/image/latest/image/copyCrop.html) というメソッドが用意されていて、以下のように元となる画像データと切り抜きたい左上の座標、そして高さと幅を指定すれば切り抜かれた画像データが戻り値として返却される、というシンプルな作りになっています。

```dart
final croppedData = image.copyCrop(
  originalImage, 40, 50, 300, 400,
),
```

たとえば↑のコードでは、  `originalImage` の左から 40 ピクセル、 上から 50 ピクセルをスタート地点として、そこから幅 300 ピクセル、高さ 400 ピクセルで画像を切り抜き、結果のデータが戻り値として返却され `croppedData` に代入される、という挙動になります。

「画像の切り抜き機能」を実現するために最終的に呼び出したいのはこの `copyCrop()` メソッドです。そのため、このメソッドに渡す「どこからどこまでを切り抜くか」のパラメータをユーザーが決める UI を提供するのがこのパッケージの主な役割になります。

## 画像の切り抜き範囲を指定する UI を作る

画像の切り抜き範囲を指定するための UI を実現するために必要な要素は大きくわけて以下の3点です。

- 画像を操作する UI 
- 範囲を指定する UI
- 指定された範囲をわかりやすく表現する UI

ということで、それぞれについて簡単に説明していきます。

### 画像を操作する UI 

おそらくこのような UI を作る上で面倒になるのが、この画像を操作する UI です。

スワイプによる表示位置の移動やピンチイン、ピンチアウト操作による拡大縮小、またその操作の結果今どの縮尺でどの位置を表示しているか、という計算など、考えるべきことはたくさんあります。

しかし、 Flutter では `InteractiveViewer` という Widget がこれらの全てを実現してくれます。

https://api.flutter.dev/flutter/widgets/InteractiveViewer-class.html

サイズの大きな Widget をこの `InteractiveViewer` の `child` として設定すれば、自動的にピンチイン/アウト、スワイプによる移動、操作の内容を伝えるコールバックなどが提供されるとても便利な Widget です。

crop_your_image パッケージでもこの Widget を使っていて、基本的にはアプリ側から受け取った画像データを `Image.memory()` で Widget にし、それを `InteractiveViewer` で動かせるようにしているだけ、という作りになっています。

特にパッケージ開発では依存する他のパッケージを少なくすることが柔軟な修正や開発には欠かせないと考えているため、 Flutter が標準でこのような Widget を提供してくれているのは本当にラッキーでした。

![qiita_demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134943/477ec665-149f-008d-40ea-0d9793c8ea9c.gif)
### 範囲を指定する UI

これは、四隅にドラッグ可能な丸くて白い `Container` を配置しているだけです。

Flutter では `Stack` によって Widget を重ねて表示でき、さらに `Positioned` によって配置する座標を決められるため、状態として保持している丸ポチの現在値を `Positioned` に渡すだけで範囲指定のコントローラー UI が実現します。

あとは、「左のポチより右のポチが左に行かない」「上のポチが画像の上端より上に行かない」などの座標的な制御を入れれば、最低限の実装が完了します。

![qiita_demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134943/15fba897-5158-6e18-f9d3-c7790e55fa9c.gif)

### 指定された範囲をわかりやすく表現する UI

上記の画像と丸ポチだけではあまり「範囲を指定している感」は出せません。

ここで必要になるのが、選択範囲は明るく、外は暗くする、という UI です。 つまり、指定した範囲をくり抜いた半透明な黒い Container を画像の上に重ねて表示する、という UI になるわけですが、 Flutter では Widget を任意の形でくり抜くための Widget として `ClipPath` と `CustomClipper` が用意されています。

`CustomClipper` はそのサブクラスを定義し、 `getClip()` メソッドの中で「くり抜き方」を指定した `Path` オブジェクトを返却することによって `ClipPath` の `child` に指定した Widget がくり抜かれる、という作りなのですが、自分の場合はパスの指定のし方からよくわかっていなかったため、以下の記事が大変参考になりました。

https://qiita.com/granoeste/items/dc6d25547c5efbba7b3f

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134943/38f51f33-d1af-b08b-e949-c16a62ab40b8.png)

これらの 3 つの UI を重ね合わせ、ユーザーの丸ポチの操作結果に合わせて `CustomClipper` で切り抜く範囲を変えると、画像切り抜き機能でよく見る以下のような画面が出来上がる、というわけです。

![qiita_demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134943/350e4807-dffd-a861-c017-6b5744f6c68a.gif)

あとはこれらの操作の結果えられた範囲や画像サイズ、 InteractiveViewer の縮尺・移動距離などから最終的に切り抜くべき座標とサイズを計算し、最初の `image.copyCrop()` の引数に渡す、というのが crop_your_image の実装です。

# まとめ

以上、僕が今試行錯誤しながら作っている画像切り抜きパッケージの紹介でした。

パッケージ開発は本当に初めての試みで、他の開発者が使いやすい設計とはどのようなものか、ライセンスをどれにするのか、 README.md には何を書くのか、どの程度の粒度でバージョンアップするのか、何か守るべきお作法は他にあるのか、などなど調べることや考えることばかりですが、ここまで少し作って公開するだけでもかなりの経験になっていると感じます。

なお、 Flutter 用のパッケージの作成方法や公開方法については公式サイトの以下のページにわかりやすく書かれています。

https://flutter.dev/docs/development/packages-and-plugins/developing-packages

やってみれば公開作業自体はとても簡単にできるため、何か作りたいものがある方は一度試してみることをオススメします。世界中の他の Flutter 開発者のことを考えてするパッケージ開発は、一般ユーザー向けのアプリ開発とはまた違った知見や経験が得られます。

そしてこの記事を読んで crop_your_image を使ってみたいと思った方はぜひ `pubspeck.yaml` に追記して使ってみてください。要望や感想などをいただけるとより開発に熱が入って良いものができていくのではないかと思います。（私の力量次第ですが）

もちろん issue やプルリクも歓迎しています。「ゴール」に書いたことを実現する手助けをいただければとても嬉しく思います。気軽に GitHub を覗いてみてください。

https://github.com/chooyan-eng/crop_your_image

今はまだアルファ版とも言えない出来ですが、良い感じに「使える」パッケージになったらまた記事にまとめたいと思います。