import 'dart:io'; import 'package:camera/camera.dart'; import 'package:flutter/foundation.dart'; import 'package:image/image.dart' as img; /// 撮影画像の保存・命名・削除を行うサービス. class FileService { static const _basePath = '/storage/emulated/0/Pictures/MiniTIAS'; /// 保存先ディレクトリのパスを返す. String get directoryPath => _basePath; /// CameraImage(YUV420)を PNG に変換して保存する. /// /// JPEG を経由せず,生データから直接 PNG に変換するため画質劣化がない. /// 保存したファイルのパスを返す. Future saveImageFromYuv(CameraImage cameraImage) async { final directory = Directory(_basePath); if (!await directory.exists()) { await directory.create(recursive: true); } // YUV → PNG 変換(重い処理なので isolate で実行) final params = { 'width': cameraImage.width, 'height': cameraImage.height, 'yPlane': Uint8List.fromList(cameraImage.planes[0].bytes), 'uPlane': Uint8List.fromList(cameraImage.planes[1].bytes), 'vPlane': Uint8List.fromList(cameraImage.planes[2].bytes), 'yRowStride': cameraImage.planes[0].bytesPerRow, 'uvRowStride': cameraImage.planes[1].bytesPerRow, 'uvPixelStride': cameraImage.planes[1].bytesPerPixel ?? 1, }; final pngBytes = await compute(_convertYuvToPng, params); final fileName = await generateFileName(); final filePath = '$_basePath/$fileName'; final file = File(filePath); await file.writeAsBytes(pngBytes); return filePath; } /// ファイル名を生成する. /// /// 形式: MiniTIAS_YYYYMMDD_HHmmss.png /// 同秒の重複がある場合は _1, _2, ... と連番を付与する. Future generateFileName() async { final now = DateTime.now(); final timestamp = '${now.year}' '${now.month.toString().padLeft(2, '0')}' '${now.day.toString().padLeft(2, '0')}' '_' '${now.hour.toString().padLeft(2, '0')}' '${now.minute.toString().padLeft(2, '0')}' '${now.second.toString().padLeft(2, '0')}'; final baseName = 'MiniTIAS_$timestamp'; final candidate = '$baseName.png'; if (!await File('$_basePath/$candidate').exists()) { return candidate; } var suffix = 1; while (await File('$_basePath/${baseName}_$suffix.png').exists()) { suffix++; } return '${baseName}_$suffix.png'; } /// テスト用: タイムスタンプとファイル名一覧からファイル名を生成する. static String generateFileNameSync( String timestamp, List existingFiles, ) { final baseName = 'MiniTIAS_$timestamp'; final candidate = '$baseName.png'; if (!existingFiles.contains(candidate)) { return candidate; } var suffix = 1; while (existingFiles.contains('${baseName}_$suffix.png')) { suffix++; } return '${baseName}_$suffix.png'; } /// YUV420 を RGB に変換し PNG エンコードする(isolate 内で実行). static Uint8List _convertYuvToPng(Map params) { final width = params['width'] as int; final height = params['height'] as int; final yPlane = params['yPlane'] as Uint8List; final uPlane = params['uPlane'] as Uint8List; final vPlane = params['vPlane'] as Uint8List; final yRowStride = params['yRowStride'] as int; final uvRowStride = params['uvRowStride'] as int; final uvPixelStride = params['uvPixelStride'] as int; final image = img.Image(width: width, height: height); for (var y = 0; y < height; y++) { for (var x = 0; x < width; x++) { final yIndex = y * yRowStride + x; final uvIndex = (y ~/ 2) * uvRowStride + (x ~/ 2) * uvPixelStride; final yValue = yPlane[yIndex]; final uValue = uPlane[uvIndex]; final vValue = vPlane[uvIndex]; final r = (yValue + 1.402 * (vValue - 128)).round().clamp(0, 255); final g = (yValue - 0.344 * (uValue - 128) - 0.714 * (vValue - 128)) .round() .clamp(0, 255); final b = (yValue + 1.772 * (uValue - 128)).round().clamp(0, 255); image.setPixelRgb(x, y, r, g, b); } } return Uint8List.fromList(img.encodePng(image)); } }