たろすの技術メモ

Jot Down the Tech

ソフトウェアエンジニアのメモ書き

Flutter × FirebaseでOGP付きURLを生成する

※この記事はFlutter × FirebaseでOGP付きURLを生成するの記事をエクスポートしたものです。内容が古くなっている可能性があります。

OGPとは

OGP とは Open Graph Protocol (オープン・グラフ・プロトコル)の略称です。 FacebookTwitterなどのSNS上でシェアされた時やシェアされたい時に、ページのタイトル、URL、概要、画像(サムネイル)を正しく伝えるためにHTMLソースに記述するタグ情報です。

引用:https://seopack.jp/seo_articles/ogp.php

TwitterなどのSNSでよく見かけるこちらのことです。

アプリのことをSNSでシェアしてもらう時に、アプリ内の情報を反映させたOGP画像を作りたい場面があると思います。今回はこちらをFlutterとFirebaseの知識だけで作る方法を共有します。

開発環境

前提

  • Flutterの新規プロジェクト作成済み
  • Firebaseの新規プロジェクト作成済み
  • FlutterプロジェクトとFirebaseプロジェクト接続済み
  • Firebase Dynamic Links設定済み
  • Firebase Storage設定済み

方針

  1. OGP画像をFlutterのCustomPainterで作成
  2. OGP画像をFirebaseStorageにUpload
  3. Uploadした画像のURL、リンクのタイトル、リンクの説明を設定したDynamic LinksをFlutter側で生成

実装

1. OGP画像をFlutterのCustomPainterで作成

CustomPainterやCanvasについては以下の記事が分かりやすいです。 https://dev.classmethod.jp/articles/flutter_custom_paint/

まずはCustomPainterを継承したOgpPainterを作成します。 ※今回は考慮していませんが、描画する要素は横幅630pxに収めた方が良い場合もあります。Twitterは横幅1200pxまで表示してくれますが、アプリによってはOGP画像の表示領域が小さく真ん中630pxしか表示されない場合もあるからです。対応する場合はCustomPainterで要素の幅が真ん中630pxを超えないように実装してください。詳しくは以下をご覧ください。 https://c3-d.jp/ogp-size-2020/

// ogp_painter.dart
import 'dart:ui' as ui;

import 'package:flutter/material.dart';

class OgpPainter extends CustomPainter {
  OgpPainter(this.logoImage);

  final ui.Image logoImage;

  @override
  void paint(Canvas canvas, Size size) {
    const sideSpace = 200.0;

    // ====================================
    // 表示テキストの設定
    // ====================================
    final textSpan = TextSpan(
      text: 'Flutter✌️',
      style: TextStyle(
        color: Colors.black.withOpacity(0.5),
        fontSize: 160,
        fontWeight: FontWeight.w400,
      ),
    );

    // ====================================
    // painterの設定
    // ====================================
    final textPainter = TextPainter(
      text: textSpan,
      textDirection: ui.TextDirection.ltr,
    );

    // ====================================
    // テキストを中心揃いにする
    // ====================================

    double centerTextPosY(double painterHeight) {
      return (size.height - painterHeight) / 2;
    }

    textPainter.layout();

    // ====================================
    // 描画処理
    // ====================================
    
    // 背景を白色にする
    final backgroundPaint = Paint()
      ..color = Color(0xffffffff)
      ..blendMode = BlendMode.color;
    canvas.drawRect(
      Rect.fromLTWH(
        0,
        0,
        size.width,
        size.height,
      ),
      backgroundPaint,
    );
    
    // Flutterのロゴ画像を描画する
    canvas.drawImage(
      logoImage,
      Offset(
        sideSpace,
        centerTextPosY(logoImage.height.toDouble()),
      ),
      Paint(),
    );
    
    // "Flutter✌️"のテキストを描画
    textPainter.paint(
      canvas,
      Offset(
        sideSpace + logoImage.width + 48,
        centerTextPosY(textPainter.height),
      ),
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}

OgpPainterの描画をByteDataに変換します。

  // main.dart
  ///
  /// OGP画像を生成
  ///
  Future<ByteData?> _createOgpImage() async {
    // OGP画像の基本サイズ
    const imageWidth = 1200;
    const imageHeight = 630;
    ui.PictureRecorder recorder = ui.PictureRecorder();
    final canvas = Canvas(recorder);

    final logoImage = await assetImageToUiImage('assets/flutter_logo.png');
    OgpPainter(logoImage).paint(
      canvas,
      Size(
        imageWidth.toDouble(),
        imageHeight.toDouble(),
      ),
    );

    final image = await recorder.endRecording().toImage(
          imageWidth,
          imageHeight,
        );
    final data = await image.toByteData(format: ui.ImageByteFormat.png);

    return data;
  }
  
  ///
  /// AssetImage -> ui.Imageに変換
  ///
  Future<ui.Image> assetImageToUiImage(String imageAssetPath) async {
    Completer<ImageInfo> completer = Completer();
    final img = AssetImage(imageAssetPath);
    img
        .resolve(ImageConfiguration())
        .addListener(ImageStreamListener((ImageInfo info, bool _) {
      completer.complete(info);
    }));
    ImageInfo imageInfo = await completer.future;
    return imageInfo.image;
  }

描画した結果が以下画像です(枠線内)。本家と見分けがつかないので【✌️ 】を付けました。

2. OGP画像をFirebaseStorageにUpload

FirebaseStorageに画像をUploadします。

  // main.dart
  ///
  /// FirebaseStorageへ画像をアップロード
  ///
  Future<Uri?> _uploadImage(ByteData data) async {
    final bytes = data.buffer.asUint8List();
    final dateString = _formattedDate();
    final ref = FirebaseStorage.instance.ref('ogp_images/$dateString.png');
    try {
      await ref.putData(
        bytes,
        SettableMetadata(
          contentType: 'image/png',
        ),
      );
      return Uri.parse('https://storage.googleapis.com/${ref.bucket}/ogp_images/$dateString.png');
    } on FirebaseException catch (e) {
      print('OGP Image Upload Error = $e');
    }
  }
  
  String _formattedDate() {
    final dateTime = DateTime.now();
    return '${DateFormat('yyyyMMddHHmmss').format(dateTime)}_${dateTime.microsecondsSinceEpoch}';
  }

※注意点 Uploadした画像は一般公開にしないとOGPで表示されないので、下記記事を参考にバケットの公開設定を変更してください。 https://qiita.com/mako0715/items/a2049d31915f10f40681#%E3%83%90%E3%82%B1%E3%83%83%E3%83%88%E3%82%92%E5%85%AC%E9%96%8B%E3%81%99%E3%82%8B

3. Uploadした画像のURL、リンクのタイトル、リンクの説明を設定したDynamic LinksをFlutter側で生成

  // main.dart
  ///
  /// DynamicLinksを生成
  ///
  Future<Uri> _buildDynamicUrl(Uri imageUrl) async {
    final DynamicLinkParameters parameters = DynamicLinkParameters(
      uriPrefix: 'https://hoge.page.link', // Dynamic Linksで作成したURL接頭辞
      link: Uri.parse('https://flutter.dev/'), // 遷移先URL
      socialMetaTagParameters: SocialMetaTagParameters(
        title: 'Flutter - Build apps for any screen',
        description: 'Flutter transforms the app development process. Build, test, and deploy beautiful mobile, web, desktop, and embedded apps from a single codebase.',
        imageUrl: imageUrl,
      ),
    );

    final dynamicUrl = await parameters.buildShortLink();

    print('Dynamic Link Short Url = ${dynamicUrl.shortUrl}');
    return dynamicUrl.shortUrl;
  }

完成です。Twitterでリンクを入力すると以下のように表示されました。

全体コード

https://github.com/taroooth/ogp_sample

以上。