画像から Exif (位置情報など) を削除しないといけない理由と Flutter での実装方法

こんにちは、株式会社 Pentagon でアプリ開発をしている Kuwano です!

本記事では、iOS、Android、Flutter Web で開発する Flutter アプリケーションにおける、Exif 情報の削除方法とその重要性について解説します。
Exif 情報は、画像データに埋め込まれたメタデータで、写真が撮影された日時やカメラの設定、場合によっては撮影地点の GPS 情報などが含まれます。
この情報が公になることが問題となる場合があります。

【こんな人に読んで欲しい】

  • Flutter アプリ開発で画像を扱うことになった人
  • オンラインで写真を共有することが多い人
  • 自分のプライバシーを保護したい人

【この記事を読むメリット】

この記事を読むことで、Exif 情報とは何か、それがなぜプライバシー問題になり得るのかを理解できます。
さらに、Flutter アプリで Exif 情報を安全に削除する方法を学ぶことができます。

【結論】

ユーザーのプライバシーを保護するためには、Exif 情報を削除することが重要です。
特に写真共有機能を持つアプリ開発者は、Exif 情報を適切に取り扱うことで、ユーザーのプライバシーリスクを軽減することが可能です。

目次

Exif とは何か

Exif は"Exchangeable image file format"の略で、デジタルカメラやスマートフォンが画像を撮影する際に、画像データとともに保存される情報の一種です。
これには、撮影日時、カメラのモデルや設定、露出、F 値、ISO 感度などの情報が含まれます。
さらに、カメラやスマートフォンによっては、撮影場所の GPS 情報まで含まれています。

参考: Exchangeable image file format

Exif を消さないといけない理由

Wikipedia に書いてあるように GPS が問題になります。

撮影時の GPS による位置情報(緯度・経度)や撮影日時など、個人情報を特定できるおそれがある情報が含まれている。例えば、撮影された写真が観光地や市街地などではなく、自宅で撮影した場合は GPS による緯度・経度がそのまま自宅の位置となる。
iPhone は、iPhone から SNS へのアップロード時やメッセージへの添付の際に位置情報のみ削除される仕様になっている。
問題点や対策 - Wikipedia

iOS は自動で位置情報が消えるみたいですが、今回は Android と web も開発要件にあったので、Exif 削除機能を実装しました。

実装

いくつかステップを踏んで解説していきます。

完全なコードはこちら

  1. FlutterWeb に対応するためにファイルを分ける
  2. Flutter と FlutterWeb で Exif を消すコードを書く

1. FlutterWeb に対応するためにファイルを分ける

Flutter では特定の環境でしか import できないようになっているライブラリが存在します。
それが、dart:iodart:htmlです。

dart:ioは FlutterMobile でしか import できません。FlutterWeb ではエラーが出ます。

同じく、dart:htmlは FlutterWeb では import できますが、FltuterMobile ではエラーが出ます

なので、このようにコードを分けて実装します

image_picker_service/
├── export.dart
├── image_picker_service.dart
├── image_picker_service_for_mobile.dart
├── image_picker_service_for_web.dart
├── image_picker_service_mixin.dart
└── compress_for_web.dart # 後ほど説明します。

export.dart

特定の環境でしか import できない問題のためにこのようにファイルを分けます。

export 'package:exif/image_picker_service/image_picker_service.dart'
    if (dart.library.html) 'package:exif/image_picker_service/image_picker_service_for_web.dart'
    if (dart.library.io) 'package:exif/image_picker_service/image_picker_service_for_mobile.dart';

これで Mobile だとimage_picker_service_for_mobileが、Web だとimage_picker_service_for_webが export されるようになります。
それ以外の環境だと、image_picker_serviceが export されます。

参考: Conditionally importing and exporting library files

image_picker_service.dart

このファイルは実際には呼び出されませんが、IDE には呼び出し元として認識されます。
なので、メソッドやコメントは適切につけておきましょう。

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';

/// このクラスは実際には呼び出されないが、IDEの補完を効かせるために定義している
class ImagePickerService {
  /// このメソッドは実際には呼び出されない
  Future<XFile?> pickImage(BuildContext context) async {
    throw UnsupportedError('');
  }

  Future<XFile?> downloadFile(String url) async {
    throw UnsupportedError('');
  }
}

image_picker_service_for_mobile.dart

このファイルには mobile 用のコードを置きます。
web では実行されないので、dart:ioを import してもエラーは出ません。

コードはこちら

image_picker_service_for_web.dart

同じく web 用のコードを定義してます。

コードはこちら

image_picker_service_mixin.dart

こちらには mobile と web の両方で使えるメソッドを定義します。

コードはこちら

2. FlutterMobile と FlutterWeb で Exif を消すコードを書く

Exif 削除と同時に圧縮もしています。
どちらかだけをしたい場合は、適宜書き換えて下さい。

FlutterMobile のコード

flutter_image_compressを使用します。

こちらの_processImageメソッド内に処理を書いています。
Web との互換性を持たせるために XFile を返すようにしています。

  Future<XFile> _processImage(
    XFile xFile,
  ) async {
    // 圧縮とExifを消す処理
    final uint8List =
        await FlutterImageCompress.compressWithFile(xFile.path, quality: 85);

    if (uint8List == null) {
      throw Exception('画像の処理に失敗しました。');
    }

    final file = File(
      (await getTemporaryDirectory()).path + p.separator + xFile.name,
    );
    final path = (await file.writeAsBytes(uint8List)).path;

    return XFile.fromData(
      uint8List,
      lastModified: await xFile.lastModified(),
      path: path,
    );
  }

FlutterWeb のコード


※ 現在はflutter_image_compressライブラリの方でweb 対応しているので、
これから書く方法を使用しなくても動くかもしれません


dart の画像圧縮ライブラリだととても動作が遅く、かなり時間がかかってしまうので、javascript で処理するようにします。
browser-image-compressionを使用します。

まずは、web/index.html の head 内にこちらを追記して下さい

<head>
  ...

  <script
    type="text/javascript"
    src="https://cdn.jsdelivr.net/npm/browser-image-compression@2.0.1/dist/browser-image-compression.js"
  ></script>
  <script src="functions.js"></script>
</head>

次に、web/functions.js ファイルを作成して、compress 関数を作成します。

function compress(path) {
  return fetch(path)
    .then((res) => res.blob())
    .then((blob) => {
      return imageCompression(blob);
    });
}

今度はjsというライブラリを使用して、dart から直接 javascript のライブラリを呼び出せるようにします。
compress_for_web.dartでこのように定義します。

@JS()
library functions;

import 'package:js/js.dart';

@JS('compress')
external Object compress(String path);

これで dart から compress 関数を呼び出せるようになりました。
image_picker_service_for_web.dartで_processImage を定義します。

  Future<XFile> _processImage(
    XFile xFile,
  ) async {
    // dartで変換すると重いのでjsで変換する
    final compressedFile =
        await promiseToFuture<html.Blob>(compress(xFile.path));

    final path = html.Url.createObjectUrlFromBlob(compressedFile);

    return XFile(
      path,
      name: p.basename('$path.jpg'),
      lastModified: await xFile.lastModified(),
    );
  }

これで iOS,Android,Web で Exif が削除できるようになりました!

完全なコードはこちら

まとめ

Exif 情報は、私たちのプライバシーを危険にさらす可能性があります。
そのため、オンラインで写真を共有する際には、Exif 情報を削除することが重要です。
また、画像投稿機能を実装する際はユーザーの個人情報が勝手に公開されないように気をつける必要があります。

おまけ

Flutterに関する知識を深めたい方には、『Flutterの特徴・メリット・デメリットを徹底解説』という記事がおすすめです。

この記事では、Flutter アプリ開発の基本から、Flutter とは何か、そして実際のFlutter アプリ 事例を通じて、その将来性やメリット、デメリットまで詳しく解説しています。
Flutterを使ったアプリ開発に興味がある方、またはその潜在的な可能性を理解したい方にとって、必見の内容となっています。

ぜひ一度ご覧ください。

採用情報はこちら
目次