import 'dart:io';
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;
/// Platform Channel から受け取った YUV データを PNG に変換して保存する.
///
/// Camera2 API でフル解像度キャプチャした生データを直接変換するため,
/// JPEG 圧縮を経由せず画質劣化がない.
Future<String> saveImageFromYuvData(Map<String, dynamic> yuvData) async {
final directory = Directory(_basePath);
if (!await directory.exists()) {
await directory.create(recursive: true);
}
final yRaw = yuvData['yPlane'];
final uRaw = yuvData['uPlane'];
final vRaw = yuvData['vPlane'];
final Uint8List yPlane;
final Uint8List uPlane;
final Uint8List vPlane;
if (yRaw is Uint8List) {
yPlane = yRaw;
uPlane = uRaw as Uint8List;
vPlane = vRaw as Uint8List;
} else {
yPlane = Uint8List.fromList((yRaw as List).cast<int>());
uPlane = Uint8List.fromList((uRaw as List).cast<int>());
vPlane = Uint8List.fromList((vRaw as List).cast<int>());
}
if (yPlane.isEmpty) {
throw Exception('YUV データが空です');
}
final params = {
'width': yuvData['width'] as int,
'height': yuvData['height'] as int,
'yPlane': yPlane,
'uPlane': uPlane,
'vPlane': vPlane,
'yRowStride': yuvData['yRowStride'] as int,
'uvRowStride': yuvData['uvRowStride'] as int,
'uvPixelStride': yuvData['uvPixelStride'] as int,
};
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<String> 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<String> 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<String, dynamic> 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);
}
}
// センサーの向き(270°)を補正するため 90° 時計回りに回転
final rotated = img.copyRotate(image, angle: 90);
return Uint8List.fromList(img.encodePng(rotated));
}
}