Flutter でWidgetから画像を生成してロゴ画像を追加する

# 概要
LINEの友達登録のQRコードや、○○payの送金先などアプリ内で作成したプロフィールや画像、ゲームのスコア、QRコードなどを共有したい場面が出てくると思います。
そういった場面で使用できる方法を紹介したいと思います。
Widgetから画像を生成するだけなら簡単で、「Widgetから画像を生成する」を見るとほぼほぼ実装できると思います。

# 実装

ここからFlutter での実装を説明していきます。

## 1. Widgetから画像を生成する

ウィジェットから画像を生成するには、画像として書き出したいウィジェットを`RepaintBoundary`で囲み、RepaintBoundaryのKeyから`RenderObject`を取得することで、画像を生成することができます。

```dart
final _globalKey = GlobalKey();

Widget build(BuildContext context) {
  return RepaintBoundary(
    key: _globalKey,
    child: SafeArea(
      child: ListView.builder(
        itemCount: 40,
        itemBuilder: _buildRow,
      ),
    ),
  );
}

// RepaintBoundary の key を渡す
Future<Uint8List> convertWidgetToImage(GlobalKey widgetGlobalKey) async {
  // RenderObjectを取得
  RenderRepaintBoundary boundary = widgetGlobalKey.currentContext.findRenderObject();
  // RenderObject を dart:ui の Image に変換する
  ui.Image image = await boundary.toImage();
  ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
  return byteData.buffer.asUint8List();
}

```

上記のコードで下記のように画像を生成することができます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91583/41ebc147-ec73-eaa3-ee89-7802c591c367.png" width="300px"/>
画像に変換したものは`Image.memory()`を使用することでWidgetとして表示することも可能です。

上の画像を見ていただきたいのですが、生成した画像の下の方を見ると「範囲内」の文字が切れているのがわかります。
この書き方では画面の描画範囲外に出ているWidgetを画像に含めることができません。
この後に画面の描画範囲外にあるWidgetも含めて画像を生成するコードを紹介します。

## 2. 画面の描画範囲外にあるWidgetも含め画像を生成する

ListViewなどを使用していると描画範囲外にWidgetがあることが多々あると思います。
それらのWidgetを含めて画像を生成するには`RepaintBoundary`を`SingleChildScrollView`で囲ってあげる必要があります。
「Widgetから画像を生成する」のサンプルコードを変更すると下記のようになります。


```dart
final _globalKey = GlobalKey();

Widget build(BuildContext context) {
  return SingleChildScrollView(
    primary: true,
    child: RepaintBoundary(
      key: _globalKey,
      child: SafeArea(
        child: ListView.builder(
          primary: false,
          shrinkWrap: true,
          itemCount: 40,
          itemBuilder: _buildRow,
        ),
      ),
    ),
  );
}
```

コードを変更して生成した画像は下記のようになります。
これで、画像を生成したい範囲内のWidgetをすべて含めることができました。
次は右下にロゴマークを追加したいと思います。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91583/63837994-2262-2d61-0b00-00e66eee59eb.png" width="300px" />

## 3. 右下にロゴを埋め込み画像を生成する

Widget の変更はありません、変更があるのは画像生成のコードです。
2つの画像から１つの画像を生成するので、Canvas を使用して画像を生成します。
Widget からui.Image を作るまでは同じです。
ここでは、PictureRecorder、Canvas、CustomPainterを使用します。


```dart:convert_to_image.dart
class ConvertWidgetToImage {
  Future<Uint8List> execute(GlobalKey widgetGlobalKey) async {
    RenderRepaintBoundary boundary = widgetGlobalKey.currentContext.findRenderObject();
    ui.Image image = await boundary.toImage();
    // Canvasを用意
    final ui.PictureRecorder recorder = ui.PictureRecorder();
    final Canvas canvas = Canvas(recorder);
    // assets の画像を読み込む
    final logoImage = await loadLocalImage();

    // Canvasで２つの画像を合成する
    ImagePainter(image, logo: logoImage)..paint(canvas, Size.infinite);
    // Canvas に描いたものを画像にする
    final ui.Image renderImage = await recorder.endRecording().toImage(
          image.width, image.height + logoImage.height,
        );
    ByteData byteData = renderImage.toByteData(format: ui.ImageByteFormat.png);
    return byteData.buffer.asUint8List();
  }

  Future<ui.Image> loadLocalImage() async {
    Completer<ImageInfo> completer = Completer();

    /// ImageProvider をui.Imageに変換している
  AssetImage('assets/logo.png').resolve(ImageConfiguration()).addListener(
          (ImageStreamListener((ImageInfo info, bool _) => completer.complete(info))),
        );
    ImageInfo imageInfo = await completer.future;
    return imageInfo.image;
  }
}
```

```dart:image_painter.dart
class ImagePainter extends CustomPainter {
  /// Widgetから生成した画像
  final ui.Image image;

  /// 下部に追加するロゴ
  final ui.Image logo;

  ImagePainter(
    this.image, {
    this.logo,
  });

  @override
  void paint(Canvas canvas, Size size) {
    // canvas の左上に画像にしたいWidgetを配置する

    canvas.drawImage(image, Offset(0, 0), Paint());

    if (logo != null) {
      // canvas の右下にロゴを配置する
      canvas.drawImage(
          logo,
          Offset(image.width.toDouble() - logo.width.toDouble(),
              image.height.toDouble() - logo.height.toDouble()),
          Paint());
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}
```

上記のコードで右下にロゴを追加した画像が生成出来ました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91583/f5de115c-bf27-eb09-94d5-589f12061714.png" width="300px" />

ロゴの位置はOffsetの値を変更することで位置を変えることができます。


## あとがき

投稿の日を1日間違えていて焦って書いたので、殴り書きになってしまいましたが上記の方法でWidgetから画像の生成ができます。
特に難しいことをしないで画像が生成できて便利ですね。
ちなみにShareプラグイン等を使用して画像を共有するには一度ローカルに保存する必要があるので、Tempディレクトリにでも保存して共有するようにしてください。

~~あとがきも投稿してから後日に書いています。次事は間に合うように書きます~~