🖼

【Flutter】内側から理解する crop_your_image パッケージ

2024/02/11に公開

2021/5/11 に最初のバージョン 0.0.1 を公開してからかれこれ 3 年近くが経過し、ようやく 2024/2/11 に stable 版として 1.0.0 が公開できましたので、改めて crop_your_image の紹介をできればと思います。

https://pub.dev/packages/crop_your_image

紹介記事は以前にも書いているので重複する部分もあるかもしれませんが、この記事では

  • パッケージの使い方とその特徴
  • パッケージを開発して得られたこと
  • パッケージのこれから

について書いていきたいと思います。

crop_your_image を使おうと検討している、使っていてもっと使いこなしたい、という方だけでなく、パッケージ作り自体に興味のある方にとっても有用な記事になることを目指して書こうと思いますので、ぜひ読んでみてください。

パッケージの使い方

まずは crop_your_image がどのようなパッケージなのかを見ていきたいと思います。

crop_your_image のイメージ

基本的な使い方

crop_your_image は SNS アプリなどでよく必要になる 画像の切り抜き機能 を提供するパッケージです。

画像を選択し、切り抜き範囲をユーザーが選択し、切り抜きボタンを実行すると選択した範囲だけを含む画像が生成されるアレです。

crop_your_image はそれを Crop という Widget を配置することで実現します。


Widget build(BuildContext context) {
  return Crop(
    image: _image, // 画像データを Uint8List 型で用意
    controller: _controller, // コントローラ
    onCropped: (croppedImage) {
      // 切り抜き後の画像を使った処理
    },
  );
}

最小限のコードはこれだけです。たとえばこのコードを New Project したばかりのプロジェクトに差し込むと以下のようになります。

Hello World のデモ

Crop は単なる Widget ですので、配置する場所や周囲の UI に制限されません。 TextField を配置するのと同じ感覚で配置できる というのがコンセプトになっているため、それが全画面であろうが、UI の一部であろうが、ダイアログであろうがどこでも切り抜き UI を埋め込めるのが crop_your_image の特徴です。[1]

ただし、このままでは「切り抜き実行ボタン」がありません。追加してあげましょう。

crop_your_image切り抜き範囲の調整作業をするための UI 以外の余計なパーツは一切用意していません ので「切り抜き実行ボタン」は自作する必要があります。その代わり、そのボタンはプロジェクトごとにデザイナーがデザインした UI で自由に用意できるようになっています。


Widget build(BuildContext context) {
  Column(
    children: [
      AspectRatio( // 高さを決めるための Widget が必要
        aspectRatio: 1,
        child: Crop(
          image: _image!,
          controller: _controller,
          onCropped: (croppedImage) {
            // Do something with cropped image
          },
        ),
      ),
      const SizedBox(height: 32),
      ElevatedButton( // 切り抜きボタン
        onPressed: () => _controller.crop(),
        child: const Text('Crop'),
      ),
    ],
  ),
}

上の例ではシンプルな ElevatedButtonColumn で縦に並べています。

なお、Crop は自身では具体的な枠の大きさを決められない Widget ですので、適宜親に AspectRatioSizedBox などを配置して指定してあげる必要があります。今回は AspectRatio を使って、画面幅いっぱいに広がる正方形の枠を作ってあげました。

切り抜きボタンを追加

ボタンがタップされると、CropController が持つ crop() メソッドが呼び出され、切り抜き処理が実行されます。

切り抜き結果は Crop のコンストラクタに渡した onCropped 関数に croppedImage として渡されますので[2]、あとはそれを次の画面に渡したり状態としてどこかで管理したり、お好きに利用できるようになっています。

以上です!これだけで最低限の切り抜き UI が任意の UI に埋め込む形で実現できるのが crop_your_image です。

interactive モード

先ほどの例では四隅のドットを上下左右に操作するという方法で切り抜く範囲を選択していました。

しかし、用途によっては画像自体をズームしたり動かしたりしながら切り抜き範囲を調節したいという要望もあると思います。

そんな時に使えるのが interactive モード です。


Widget build(BuildContext context) {
  return Crop(
    image: _image, 
    controller: _controller,
    onCropped: (croppedImage) {},
    interactive: true, // interactive モードをオンに
    fixCropRect: true, // 切り抜き枠は固定
    cornerDotBuilder: (_, __) => const SizedBox.shrink(), // 四隅のドットは非表示
  );
}

interactivetrue を渡すことで画像の移動とズームが可能になります。使い勝手を考えると、fixCropRecttrue にして枠を固定し、cornerDotBuilder で四隅の ● を SizedBox.shrink() に変更して消してあげる対応もセットで入れると良いとでしょう。

interactiveモード

画像の細かな部分をしっかりと選んで切り抜きたい場合はこの interactive モードが便利だと思います。

その他のカスタマイズ

Crop は他にもいろいろなカスタマイズが可能です。

たとえば切り抜き枠の初期位置を指定するための initialRectBuilder, initialSize, initialArea や枠の縦横比を固定する aspectRatio、丸画像を切り抜くための withCircleUiCrop 自体の見た目を調節する maskColor, baseColor, radius, progressIndicator などが用意されています。

またこれらの値のいくつかはリビルドで宣言的に変化させることが可能になっており、そうでないものも CropController から命令的に変更できるようになっています。

Crop は単なる Widget なので、Crop の上に重ねて何かを表示したい場合はそのまま Stack を使えば良いでしょう。[3]

さらに上級者向けの機能として、切り抜きロジック自体を差し替える ための imageCropper, formatDetector, imageParser などの引数も用意されています。

たとえば切り抜き範囲の座標をユーザーが選ぶところまでは Crop が行い、実際の切り抜き処理は自前のサーバー側で実装した処理に任せたい、という場合は ImageCropper インターフェースを実装したクラスを用意し、

/// 独自の切り抜きロジックを持ったクラス
class MyImageCropper extends ImageCropper<Image> {
  const ImageImageCropper();

  
  Future<Uint8List> call({
    required Image original,
    required Offset topLeft,
    required Offset bottomRight,
    ImageFormat outputFormat = ImageFormat.jpeg,
    ImageShape shape = ImageShape.rectangle,
  }) async {
    // 1. 受け取った条件でサーバー側に切り抜き処理を依頼
    // 2. サーバーから返却された切り抜き済み画像を Uint8List 型で return
  }
}

そのインスタンスを CropimageCropper 引数に渡してあげれば処理が上書きできるようになっています。


Widget build(BuildContext context) {
  return Crop(
    image: _image, 
    controller: _controller,
    onCropped: (croppedImage) {},
    imageCropper: const MyImageCropper(), // ロジックを上書き
  );
}

crop_your_image ではデフォルトで image パッケージを利用していますが、 画像処理に関する資産があるプロジェクトではその資産を活用できるよう、ロジック自体も差し替え可能な作り にしています。


以上がおおまかなパッケージの使い方です。

各パラメータの細かな説明は Readme にも記載していますので、ぜひ pub.dev のページを開いてみてください。(そして LIKE ボタンを押していただけると嬉しいです)

https://pub.dev/packages/crop_your_image

パッケージを開発して得られたこと

ここからはパッケージ開発をしての体験談を書いていきたいと思います。

「思想」は大事

3年前に「画像切り抜きパッケージ」を作ってみよう、と思ったとき、すでに同じジャンルのパッケージはいくつも公開されていました。

自分は画像処理に知見のある開発者ではないですので、そんな状況で自分がパッケージを作ることに意味があるのだろうかと考えながら既存のパッケージを調査していたのですが、よくよく観察していると Flutter っぽい作りになってるパッケージって少ないんだなと感じた のがこのパッケージのスタートでした。

多くのパッケージは静的メソッドを呼び出すことでネイティブで実装した画面に遷移し、決まった UI の中で切り抜き処理をして完了すると元の画面に戻ってくる、という作りになっているものが多いと感じられました。おそらくネイティブの資産やノウハウをそのまま Flutter に転用したのではないかと推測しています。

しかしわれわれが触っているのは "Everything is a Widget" の思想の上に成り立つ Flutter です。それならば 切り抜き UI もひとつの Widget として実現したい な、というのが crop_your_image パッケージの思想です。

思想が決まるといろいろなことが見えてきます。

  • あるべき API デザイン
  • パッケージがやること、やらないこと
  • Readme に書くべきアピールポイント

それぞれについて crop_your_image を例に見ていきます。

API デザイン

API デザインを考えるにあたり Widget らしさとは何か ということを考えました。

Text のようにどこでも置けて、Theme のように Widget ツリーを利用した見た目の調整ができて、TextEditingController のように命令的な処理がたまに出てくる、というのが自分の中の「Widget らしさ」のイメージです。

これに沿って作ることで、パッケージを使うアプリ開発者にとっても使いやすい API になるのではないかと考えたのが Crop という Widget と CropController というコントローラです。

リビルドで UI を変化できるようにも気をつけていて、たとえば maskColorbaseColor といった引数はリビルド時に別の値が渡ることで即座に変化が UI に反映されます。

一方で切り抜き枠のような、計算を伴うためにリビルドごとに UI に反映できないものについては CropController から命令的に変化させる作りになっています。これは TextEditingController.textTextField の入力文字列を変化させる作りを参考にしています。

このように、なるべく Flutter 標準の Widget たちと同じ感覚で使える ことを API デザインとして目指しています。

パッケージがやること、やらないこと

パッケージがやることとして、crop_your_image画像の切り抜き機能 として明確に線引きしています。

つまり、以下のような機能は将来的にも入れる予定はありません。

  • カメラロールなどからの画像の読み込み
  • 画像の回転やフィルタなどの加工

パッケージがやる範囲を明確にすることで、ユーザーとしてもどこまでは自分でコーディングする(もしくは別パッケージを探す)必要があるのかが明確になり、開発側としても無駄にコードベースが大きくならず、保守しやすい規模を保てます。

「全部入り」が欲しい人はそれこそ既存の画像切り抜きパッケージがあるため、crop_your_image は利用者にとってのカスタマイズのしやすさと開発側にとっての保守のしやすさを優先する形になっています。

Readme に書くべきアピールポイント

ということを Readme の最初に Philosophy として記載しています。

Readme の最初の段落はおそらく多くの利用者がそのパッケージを把握するために読む部分ですので、 「思想」として明確に記載しておくことで他のパッケージとの立ち位置の違いを明確にし、利用者が使うかどうかを判断しやすくなる 効果を狙っています。

同じようなパッケージが複数ある場合は、その機能の豊富さを列挙するよりも 「思想」が明確であることの方が取捨選択が楽 になり、結果としてユースケースがフィットする人に選んでもらえてうまく使ってくれる(想定外の使い方をされて想定外の問題や要望が発生するリスクが減る)ことにつながるのではないかと考えています。

時間がとれない、、

といろいろ書きましたが、この 3 年間のうちの 9 割以上は「放置期間」でした、、

SNS 上でもたびたび上がる話題ですが、やはり OSS は開発者の時間とモチベーションの確保が重要な課題 です。

開発して使ってもらえたとしても仕事と違ってお金にはならない、だから仕事は仕事でやらなければならないけど今度はパッケージを開発する時間と余力がなくなる、結果として放置されて使われなくなる、使われなくなるのでモチベーションが下がる。一方で issue とプルリクは溜まってさらに焦る。

という負のスパイラルに陥っていたのがこの 3 年間で、今もそれが解決したわけではありまん。たまたまふとモチベーションが出てきたこのタイミングで少し無理して開発している、というのが正直な状況です。[4](この記事も夜 2 時に夜ふかしして書いています)

バージョンが 1.0.0 になったこのタイミングでもまだちゃんと回答できていない issue やプルリクが残っている状況ですので、このあたりは引き続き課題になりそうです。

何か良い知恵がある方はぜひ教えてください、、!

技術的な知見

もちろん良いこともたくさんあります。そのうちの 1 つが技術的な知見を深められることです。

crop_your_image の場合は、

  • ジェスチャと座標計算を伴う処理の実装方法
  • 使いやすい共通クラスの設計方法
  • Widget、ロジックそれぞれに対するテストコードの書き方
  • Fork されたリポジトリから飛んでくるプルリクの処理方法
  • Web, Desktop の対応

などについて考える良い機会となり、そこで得られた知見の中には仕事でも活用できるものが少なくありません。

特にテストコードなどはちゃんと考えながら書いた経験があまりなかったため、Widget からロジックまで最低限のテストケースを書くことができてだいぶ勉強になりました。意外と Crop のようなユーザー操作を伴う Widget に対してもテストコードが書けるのは新感覚でした。

逆に本業で試行錯誤したアーキテクチャに対する考え方をこちらのパッケージ開発にも転用できたりと、知見を相互に輸入しあってお互いの品質を高められた のは良い成果だと思っています。

知見が溜まればネタにもなりますので、勉強会の登壇や記事を書いたりもしやすくなりました。

小さなサンプルアプリを書いて動作確認して、という勉強方法に飽きてきたら、何か 具体的な「自分のパッケージ」を作ることでさらに大きな勉強になるはず です。

crop_your_image のこれから

ひとまず 1.0.0 のこの段階で、ある程度使いやすくある程度テストされある程度変更を加えやすい状態にはなったのではないかなと思います。

現状の課題として

  • Crop の引数が多すぎてどれをどう使えば良いのか分かりづらい
  • もっと宣言的な使い方に寄せられないかどうか
  • テストコードがまだまだ足りない
  • issue やプルリクが溜まっている。新しい機能の提案も来ている。

などやるべきことややりたいことは多い状態ですので、引き続きゆるゆると開発は続ける予定です。これを読んだみなさんからのプルリクなどもお待ちしています!

まとめ

ということで、crop_your_image パッケージの使い方から開発の裏話まで、文字通り「内側」から理解いただけたのではないかなと思います。

「ほんとにこんなの作れるんだろうか」と思いながらコードを書き始めたのがこのパッケージも気がついたら LIKE が 400 近くまで増え、 "crop image" で pub.dev を検索するとトップに表示されるようになり(!)、いろいろな方から「使ってます!」と言っていただけていて、我ながらよく作ったなというのが正直な感想です。

"crop image"で検索1位!

pub.dev には「このパッケージに依存しているパッケージ」を見る画面もあるのですが、それを見るとなんと 6 つほどリストアップされているのも嬉しいところです。

https://pub.dev/packages?q=dependency%3Acrop_your_image

開発の時間をとりづらい状況は解決したわけではありませんが、引き続き使いやすい画像切り抜きパッケージにしていきたいと思いますので、興味があればぜひ触ってみてください。いつか「crop_your_image 使ってみた」の記事が表れるのを心待ちにしています。

脚注
  1. 画像切り抜きパッケージの中には、static なメソッドを呼び出すとネイティブで実装された「切り抜き画面」に遷移するものも多いですが、もっと Flutter の "Every thing is a Widget" のアイデアに沿った作りにしたいよね、という考えです。 ↩︎

  2. crop() メソッド自体は切り抜き処理をキックするだけで結果が戻り値で返ってきたりはしません。このあたりは TextFieldonChange などの作りをイメージしています。 ↩︎

  3. タッチ操作を奪ってしまわないよう、IgnorePointer を適宜配置してください。 ↩︎

  4. その意味では、この記事を書いている Zenn というプラットフォームはアウトプットした開発者にお金が入る仕組みが無理なく実装されているのが素晴らしいと思っています。 ↩︎

GitHubで編集を提案

Discussion