🌅

【Flutter】Image.networkで表示する画像の背景を透過させる

2025/01/17に公開

はじめに

Flutterアプリケーションで画像を表示する際、背景を透過させたいケースは多くあります。例えば、

  • 商品画像のカタログ表示
  • プロフィール画像の表示
  • アイコンやロゴの表示

この記事では、Image.networkで読み込む画像の背景を透過させる実装方法について解説します。

before after

実装の概要

実装のポイントは以下の3つです:

  1. 画像データをピクセルレベルで処理
  2. 指定した背景色に近い色を持つピクセルを透過
  3. 処理した画像を新しく生成して表示

実装

pubspec.yamlに以下のパッケージを追加します:

dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0  # ネットワーク画像の取得用

透過Widget

import 'dart:developer';
import 'dart:typed_data';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

class BackgroundRemover extends StatefulWidget {
  final String imageUrl;
  final double tolerance; // 白色の許容範囲(0.0 〜 1.0)

  const BackgroundRemover({
    Key? key,
    required this.imageUrl,
    this.tolerance = 0.1,
  }) : super(key: key);

  
  State<BackgroundRemover> createState() => _BackgroundRemoverState();
}

class _BackgroundRemoverState extends State<BackgroundRemover> {
  ui.Image? processedImage;

  
  void initState() {
    super.initState();
    _processImage();
  }

  Future<void> _processImage() async {
    try {
      // 画像をネットワークから読み込む
      final response = await http.get(Uri.parse(widget.imageUrl));
      final Uint8List bytes = response.bodyBytes;

      // デコードして画像オブジェクトを取得
      final codec = await ui.instantiateImageCodec(bytes);
      final frame = await codec.getNextFrame();
      final image = frame.image;

      // 画像データを取得
      final byteData = await image.toByteData();
      if (byteData == null) return;

      // RGBA形式のデータに変換
      final pixels = byteData.buffer.asUint32List();

      // 各ピクセルを処理
      for (var i = 0; i < pixels.length; i++) {
        final color = Color(pixels[i]);

        // RGBの値が閾値以上なら透明に
        if (isNearWhite(color, widget.tolerance)) {
          pixels[i] = 0x00000000; // 完全な透明
        }
      }

      // 新しい画像を作成
      final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(
        pixels.buffer.asUint8List(),
      );

      final descriptor = ui.ImageDescriptor.raw(
        buffer,
        width: image.width,
        height: image.height,
        pixelFormat: ui.PixelFormat.rgba8888,
      );

      final processedCodec = await descriptor.instantiateCodec();
      final processedFrame = await processedCodec.getNextFrame();

      setState(() {
        processedImage = processedFrame.image;
      });
    } catch (e) {
      log('Error processing image: $e');
    }
  }

  bool isNearWhite(Color color, double tolerance) {
    final threshold = 255 * (1 - tolerance);
    return color.red > threshold &&
        color.green > threshold &&
        color.blue > threshold;
  }

  
  Widget build(BuildContext context) {
    if (processedImage == null) {
      return const Center(child: CircularProgressIndicator());
    }

    return RawImage(
      image: processedImage,
      fit: BoxFit.contain,
    );
  }
}

使用する

BackgroundRemover(
  imageUrl: '画像URL',
  tolerance: 0.1 // 白の許容量(0.0 〜 1.0で透過を調節できます)
);

まとめ

これでダークモードでも不自然じゃなくなるかも🥳

Discussion