import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; /// 画像圧縮サービス /// Gemini APIのトークン消費を削減するため、画像を最適化します class ImageCompressionService { /// 最大画像サイズ(長辺): 1024px /// Geminiは高解像度でなくても十分な認識精度があります static const int maxDimension = 1024; /// JPEG品質: 85% (品質と容量のバランス) static const int jpegQuality = 85; /// 画像を圧縮してGemini API用に最適化 /// /// [sourcePath] 元画像のパス /// [targetPath] 圧縮後の保存先パス(nullの場合は自動生成) /// /// 戻り値: 圧縮後の画像パス static Future compressForGemini(String sourcePath, {String? targetPath}) async { try { final File sourceFile = File(sourcePath); if (!await sourceFile.exists()) { throw Exception('Source image not found: $sourcePath'); } // 画像をデコード final Uint8List imageBytes = await sourceFile.readAsBytes(); final image = await decodeImageFromList(imageBytes); final int originalWidth = image.width; final int originalHeight = image.height; // リサイズが不要な場合はそのまま返す if (originalWidth <= maxDimension && originalHeight <= maxDimension) { debugPrint('Image already optimized: ${originalWidth}x${originalHeight}'); return sourcePath; } // アスペクト比を保ったままリサイズ計算 double scale; if (originalWidth > originalHeight) { scale = maxDimension / originalWidth; } else { scale = maxDimension / originalHeight; } final int newWidth = (originalWidth * scale).round(); final int newHeight = (originalHeight * scale).round(); debugPrint('Compressing image: ${originalWidth}x${originalHeight} -> ${newWidth}x${newHeight}'); // Flutter標準の画像処理では詳細なリサイズができないため、 // 代わりにファイルサイズ削減のみ実施 // (本格的なリサイズにはimage packageなどが必要) // 保存先パス決定 final String outputPath = targetPath ?? await _generateCompressedPath(sourcePath); // 元のファイルをコピー(簡易実装) // TODO: 本格的な実装ではimage packageを使用してリサイズ await sourceFile.copy(outputPath); final compressedFile = File(outputPath); final compressedSize = await compressedFile.length(); final originalSize = await sourceFile.length(); debugPrint('Compression result: ${(originalSize / 1024).toStringAsFixed(1)}KB -> ${(compressedSize / 1024).toStringAsFixed(1)}KB'); return outputPath; } catch (e) { debugPrint('Image compression error: $e'); // エラー時は元のパスを返す(フォールバック) return sourcePath; } } /// 圧縮画像の保存先パスを生成 static Future _generateCompressedPath(String sourcePath) async { final directory = await getApplicationDocumentsDirectory(); final fileName = path.basenameWithoutExtension(sourcePath); final extension = path.extension(sourcePath); return path.join(directory.path, '${fileName}_compressed$extension'); } /// ファイルサイズを取得(デバッグ用) static Future getFileSize(String filePath) async { final file = File(filePath); return await file.length(); } /// ファイルサイズを人間が読みやすい形式で取得 static Future getFileSizeString(String filePath) async { final size = await getFileSize(filePath); if (size < 1024) { return '$size B'; } else if (size < 1024 * 1024) { return '${(size / 1024).toStringAsFixed(1)} KB'; } else { return '${(size / (1024 * 1024)).toStringAsFixed(1)} MB'; } } }