ponshu-room-lite/lib/services/image_compression_service.dart

196 lines
7.7 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
import 'package:image/image.dart' as img; // CR-005: 画像圧縮・リサイズ用
/// 画像圧縮サービス
/// 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<String> compressForGemini(String sourcePath, {String? targetPath}) async {
try {
final File sourceFile = File(sourcePath);
if (!await sourceFile.exists()) {
throw Exception('Source image not found: $sourcePath');
}
// CR-005: 画像をデコードimage packageを使用
final Uint8List imageBytes = await sourceFile.readAsBytes();
final img.Image? originalImage = img.decodeImage(imageBytes);
if (originalImage == null) {
debugPrint('Failed to decode image, returning original');
return sourcePath;
}
final int originalWidth = originalImage.width;
final int originalHeight = originalImage.height;
// リサイズが不要な場合は、圧縮のみ実施
if (originalWidth <= maxDimension && originalHeight <= maxDimension) {
debugPrint('Image dimensions OK: ${originalWidth}x$originalHeight, applying JPEG compression');
// 保存先パス決定
final String outputPath = targetPath ?? await _generateCompressedPath(sourcePath);
// JPEG圧縮のみ適用
final compressedBytes = img.encodeJpg(originalImage, quality: jpegQuality);
await File(outputPath).writeAsBytes(compressedBytes);
final originalSize = imageBytes.length;
final compressedSize = compressedBytes.length;
debugPrint('Compression result: ${(originalSize / 1024).toStringAsFixed(1)}KB -> ${(compressedSize / 1024).toStringAsFixed(1)}KB');
return outputPath;
}
// CR-005: リサイズが必要な場合
debugPrint('Resizing image: ${originalWidth}x$originalHeight -> max $maxDimension');
// アスペクト比を保ったままリサイズ
final img.Image resized = img.copyResize(
originalImage,
width: originalWidth > originalHeight ? maxDimension : null,
height: originalHeight > originalWidth ? maxDimension : null,
interpolation: img.Interpolation.linear, // 高品質な補間
);
// 保存先パス決定
final String outputPath = targetPath ?? await _generateCompressedPath(sourcePath);
// JPEG形式で保存
final compressedBytes = img.encodeJpg(resized, quality: jpegQuality);
await File(outputPath).writeAsBytes(compressedBytes);
final originalSize = imageBytes.length;
final compressedSize = compressedBytes.length;
debugPrint('Resize & Compression result: ${originalWidth}x$originalHeight (${(originalSize / 1024).toStringAsFixed(1)}KB) -> ${resized.width}x${resized.height} (${(compressedSize / 1024).toStringAsFixed(1)}KB)');
return outputPath;
} catch (e) {
debugPrint('Image compression error: $e');
// エラー時は元のパスを返す(フォールバック)
return sourcePath;
}
}
/// 圧縮画像の保存先パスを生成(一時ディレクトリ)
///
/// 🔒 一時ファイルは getTemporaryDirectory() に保存
static Future<String> _generateCompressedPath(String sourcePath) async {
final directory = await getTemporaryDirectory();
final fileName = path.basenameWithoutExtension(sourcePath);
final extension = path.extension(sourcePath);
return path.join(directory.path, '${fileName}_compressed$extension');
}
/// ファイルサイズを取得(デバッグ用)
static Future<int> getFileSize(String filePath) async {
final file = File(filePath);
return await file.length();
}
/// ファイルサイズを人間が読みやすい形式で取得
static Future<String> 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';
}
}
/// ギャラリー保存用に画像を圧縮(高品質版)
///
/// [sourcePath] 元画像のパス
/// [targetPath] 圧縮後の保存先パス(nullの場合は自動生成)
/// [maxDimension] 最大画像サイズ(デフォルト: 2000px)
/// [quality] JPEG品質(デフォルト: 90%)
///
/// 戻り値: 圧縮後の画像パス
static Future<String> compressForGallery(
String sourcePath, {
String? targetPath,
int maxDimension = 2000,
int quality = 90,
}) 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 img.Image? originalImage = img.decodeImage(imageBytes);
if (originalImage == null) {
debugPrint('Failed to decode image, returning original');
return sourcePath;
}
final int originalWidth = originalImage.width;
final int originalHeight = originalImage.height;
// リサイズが不要な場合は、圧縮のみ実施
if (originalWidth <= maxDimension && originalHeight <= maxDimension) {
debugPrint('Gallery: Image dimensions OK: ${originalWidth}x$originalHeight, applying JPEG compression');
final String outputPath = targetPath ?? await _generateCompressedPath(sourcePath);
final compressedBytes = img.encodeJpg(originalImage, quality: quality);
await File(outputPath).writeAsBytes(compressedBytes);
final originalSize = imageBytes.length;
final compressedSize = compressedBytes.length;
debugPrint('Gallery compression: ${(originalSize / 1024).toStringAsFixed(1)}KB -> ${(compressedSize / 1024).toStringAsFixed(1)}KB');
return outputPath;
}
// リサイズが必要な場合
debugPrint('Gallery: Resizing image: ${originalWidth}x$originalHeight -> max $maxDimension');
final img.Image resized = img.copyResize(
originalImage,
width: originalWidth > originalHeight ? maxDimension : null,
height: originalHeight > originalWidth ? maxDimension : null,
interpolation: img.Interpolation.cubic, // ギャラリー用は最高品質
);
final String outputPath = targetPath ?? await _generateCompressedPath(sourcePath);
final compressedBytes = img.encodeJpg(resized, quality: quality);
await File(outputPath).writeAsBytes(compressedBytes);
final originalSize = imageBytes.length;
final compressedSize = compressedBytes.length;
debugPrint('Gallery resize & compression: ${originalWidth}x$originalHeight (${(originalSize / 1024).toStringAsFixed(1)}KB) -> ${resized.width}x${resized.height} (${(compressedSize / 1024).toStringAsFixed(1)}KB)');
return outputPath;
} catch (e) {
debugPrint('Gallery image compression error: $e');
return sourcePath;
}
}
}