2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Flutterのスクリーンショットにデバイスフレームを合成するGo製ツールとCI/CDの話🐱

Posted at

要約

Flutterの統合テストで撮影したスクリーンショットに、デバイスフレーム(端末の外観)を合成するGo製のCLIツール merge_frame を作成しました。

また、GitHub Actionsを用いてクロスプラットフォームなバイナリのビルドとGitHubへのリリースを自動化したので、その知見を共有します。

はじめに

アプリをリリースするには、アプリの内容が分かるスクリーンショットが必要になります。大体、3〜10枚程度が相場です。

いちいち手動で撮っていると大変なため、我々はFlutterの統合テストなどを活用し、スクリーンショットの作成を自動化しています。

統合テストによるスクリーンショットの自動作成については他の記事に譲ります。ここでは、作成後の処理について説明します。

Flutterの統合テストでスクリーンショットを撮ると、デバイスフレーム(端末の外観)が含まれず、アプリストア用の素材として使用する際にプロフェッショナルな印象を与えにくいという課題があります。手動でデバイスフレームを加工するのは時間がかかり、頻繁にアップデートしたり、複数のスクリーンショットを処理する際には特に非効率です。
この記事では、その課題を解決するために開発したCLIツール merge_frame と、そのリリースプロセスを自動化するCI/CDパイプラインについて紹介します。

対象読者

  • Flutterアプリのリリース用スクリーンショット作成を効率化したい方
  • GoでクロスプラットフォームなCLIツール開発に興味がある方
  • GitHub Actionsでビルドやリリースを自動化したい方

merge_frameの紹介

作成したリポジトリはこちらです。コードを参考にしたい方はぜひこちらからどうぞ。

以下のように使用できるCLIツールです。

merge_frame --screenshots ./screenshots --frame ./frame.png --output ./output

処理前後の比較:

before after

フレーム画像は公式からダウンロードできます。

https://developer.apple.com/jp/design/resources/

Goを採用した理由

当初はPythonも検討しましたが、以下の理由からGoを採用しました:

  • クロスコンパイルが容易: 単一のバイナリを各OS・アーキテクチャ向けに簡単に生成できます
  • 環境構築のシンプルさ: 実行に際して、ユーザー側にGoや特定のライブラリのインストールを要求しないため、配布が容易です
  • 高い保守性: 静的型付け言語であり、コンパイル時のエラー検出、明示的なエラーハンドリング、依存関係の管理が優秀で、長期的なメンテナンスに適しています
  • 優秀な標準ライブラリ: 画像処理やファイル操作などの基本機能が標準で提供されており、外部依存を最小限に抑えられます

技術実装の詳細

使用ライブラリ

今回のプロジェクトでは、以下のGoライブラリを採用しました:

  • urfave/cli/v2: CLIアプリケーションのフレームワーク。フラグの定義やバリデーション、ヘルプ機能を簡潔に実装できます
  • fogleman/gg: 2Dグラフィックライブラリ。角丸描画やクリッピング機能により、現代的なスマートフォンの画面形状に対応した合成処理を実現しています

核となる画像合成ロジック

画像の合成処理では、以下のアルゴリズムを採用しています:

// calculateOffset calculates the offset needed to center image b within image a.
func calculateOffset(a image.Image, b image.Image) (int, int) {
	aw, ah := a.Bounds().Dx(), a.Bounds().Dy()
	bw, bh := b.Bounds().Dx(), b.Bounds().Dy()
	return (aw - bw) / 2, (ah - bh) / 2
}

// processScreenshot processes a single screenshot by overlaying it onto the frame.
func processScreenshot(frameImg image.Image, screenshotPath string, outputDir string) error {
	img, err := loadImage(screenshotPath)
	if err != nil {
		log.Printf("画像の読み込みに失敗しました: %v", err)
		return err
	}

	output := image.NewRGBA(frameImg.Bounds())
	offsetX, offsetY := calculateOffset(frameImg, img)

	dc := gg.NewContextForRGBA(output)
	// 角丸クリッピング
	dc.DrawRoundedRectangle(float64(offsetX), float64(offsetY),
		float64(img.Bounds().Dx()), float64(img.Bounds().Dy()), 80)
	dc.Clip()

	dc.DrawImage(img, offsetX, offsetY)
	draw.Draw(output, output.Bounds(), frameImg, image.Point{}, draw.Over)

	// ... 保存処理
}

この実装により、スクリーンショットをフレーム画像の中央に配置し、角丸処理を適用してリアルなデバイス表示を再現しています。

角丸にしているのは、画面の角がフレーム画像からはみ出してしまうためです。

GitHub Actionsによるリリース自動化

このツールは実際に使用してもらわないと意味がないため、GitHub Actionsによるリリースプロセスの自動化を実装しました。タグをpushするだけで、各プラットフォーム向けのバイナリが自動でビルドされ、GitHub Releaseにアップロードされる仕様です。

これにより、チームメンバーは簡単に既存のプロジェクトにこのバイナリを組み込むことができます。
クロスプラットフォーム対応もしているため、GitHub ActionsのようなLinux環境でも実行可能です。

ワークフロー

ビルドしてリリースするワークフローは以下の通りです:

name: Build and Release

on:
  push:
    branches: [ main ]
    tags:
      - 'v*'
  pull_request:

permissions:
  contents: write

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        os: [linux, windows, darwin]
        arch: [amd64, arm64]
    steps:
      - uses: actions/checkout@v4
      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.24'
      - name: Install dependencies
        run: go mod tidy
      - name: Build binary
        run: |
          ext=""
          if [ "${{ matrix.os }}" = "windows" ]; then ext=".exe"; fi
          GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} go build -o merge_frame-${{ matrix.os }}-${{ matrix.arch }}${ext} main.go
      - name: Archive artifact
        uses: actions/upload-artifact@v4
        with:
          name: merge_frame-${{ matrix.os }}-${{ matrix.arch }}
          path: merge_frame-${{ matrix.os }}-${{ matrix.arch }}*

  release:
    needs: build
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')
    steps:
      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts
      - name: Create Release
        uses: softprops/action-gh-release@v2
        with:
          files: artifacts/**/*
          draft: false
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

全体的な流れとしては、まずアーティファクトという形でビルドしたバイナリをGitHubに保存し、その後にリリースジョブでそれらをまとめてGitHub Releaseにアップロードします。

ちなみに、リリース名はデフォルトでタグ名と同じになります。

image.png
https://github.com/softprops/action-gh-release/tree/v2/?tab=readme-ov-file#-customizing

アーティファクトとは、GitHub Actionsのジョブで生成されたファイルやディレクトリのことを指します。actions/upload-artifact@v4actions/download-artifact@v4を用いると、アップロードしたりダウンロードしたりすることができます。

また、draft: falseにすることで、リリースを自動的に公開状態にしています。必要に応じてdraft: true(デフォルト値)に変更し、手動で公開することも可能です。

細かい点として、secrets.GITHUB_TOKENはデフォルトで提供されているため、自分で設定する必要はありません。

アーティファクトの保存(アップロード)には制限があります。Publicリポジトリなら無制限ですが、Privateリポジトリの場合、100MBが制限になっています。

アーティファクトはActionsタブの各ジョブから確認・ダウンロードできます。

image.png

リリースは以下のような見た目になります。

image.png

まとめ

本記事では、Flutterのスクリーンショットにデバイスフレームを合成する自作のGo製ツールmerge_frameと、そのリリースを自動化するGitHub Actionsのワークフローについて紹介しました。

得られた知見

  • Goのクロスコンパイル機能はCLIツールの配布に非常に便利で、ユーザーの環境構築負荷を大幅に削減できる
  • urfave/clifogleman/ggの組み合わせにより、実用的な画像処理CLIツールを効率的に開発できる
  • GitHub Actionsでビルドとリリースを分離し、アーティファクト経由で連携させることで、効率的で安全なCI/CDパイプラインを構築できる

アーティファクトの存在がかなり便利だと感じました。

この記事が、同じような課題を持つ方や、Go言語でのCLIツール開発に興味のある方、CI/CDで悩んでいる方の助けになれば幸いです。

参考

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?