import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; /// 撮影画像の保存・命名・削除を行うサービス. class FileService { static const _basePath = '/storage/emulated/0/Pictures/MiniTIAS'; /// 保存先ディレクトリのパスを返す. String get directoryPath => _basePath; /// [directoryPath] が存在しない場合,再帰的に作成する. Future _ensureDirectory() async { final directory = Directory(directoryPath); if (!await directory.exists()) { await directory.create(recursive: true); } } /// `YYYYMMDD_HHmmss` 形式のタイムスタンプ文字列を生成する. static String _buildTimestamp(DateTime dt) => '${dt.year}' '${dt.month.toString().padLeft(2, '0')}' '${dt.day.toString().padLeft(2, '0')}' '_' '${dt.hour.toString().padLeft(2, '0')}' '${dt.minute.toString().padLeft(2, '0')}' '${dt.second.toString().padLeft(2, '0')}'; /// PNG バイト列をファイルに書き込んで保存する. /// /// Android ネイティブ側で YUV→RGB→PNG 変換済みのバイト列を受け取り, /// そのまま書き込むため高速に動作する. Future saveImage(Uint8List pngBytes) async { await _ensureDirectory(); final fileName = await generateFileName(); final filePath = '$directoryPath/$fileName'; await File(filePath).writeAsBytes(pngBytes); return filePath; } /// 候補名が衝突した場合に `_1`, `_2`, ... と連番サフィックスを付与し, /// 一意な名前を決定する(同期版). /// /// [baseName] 自体が衝突しなければ [baseName] を返し,衝突する場合は /// suffix=1 から順に空きを探して `${baseName}_$suffix` を返す. /// 衝突判定(拡張子の付与等)は [exists] クロージャが吸収する. static String _resolveUniqueName( String baseName, bool Function(String name) exists, ) { if (!exists(baseName)) { return baseName; } var suffix = 1; while (exists('${baseName}_$suffix')) { suffix++; } return '${baseName}_$suffix'; } /// [_resolveUniqueName] の非同期版.[exists] が `Future` を返す点のみが異なる. static Future _resolveUniqueNameAsync( String baseName, Future Function(String name) exists, ) async { if (!await exists(baseName)) { return baseName; } var suffix = 1; while (await exists('${baseName}_$suffix')) { suffix++; } return '${baseName}_$suffix'; } /// ファイル名を生成する. /// /// 形式: MiniTIAS_YYYYMMDD_HHmmss.png /// 同秒の重複がある場合は _1, _2, ... と連番を付与する. Future generateFileName() async { final baseName = 'MiniTIAS_${_buildTimestamp(DateTime.now())}'; final resolved = await _resolveUniqueNameAsync( baseName, (name) => File('$directoryPath/$name.png').exists(), ); return '$resolved.png'; } /// カメラ診断情報を JSON ファイルとして保存する. /// /// ファイル名: `camera_diagnostics_YYYYMMDD_HHmmss.json` /// 保存先: `/storage/emulated/0/Pictures/MiniTIAS` /// 戻り値は保存したファイルのフルパス. Future saveDiagnosticsJson(Map diagnostics) async { await _ensureDirectory(); final fileName = 'camera_diagnostics_${_buildTimestamp(DateTime.now())}.json'; final filePath = '$directoryPath/$fileName'; const encoder = JsonEncoder.withIndent(' '); await File(filePath).writeAsString(encoder.convert(diagnostics)); return filePath; } /// テスト用: タイムスタンプとファイル名一覧からファイル名を生成する. static String generateFileNameSync( String timestamp, List existingFiles, ) { final baseName = 'MiniTIAS_$timestamp'; final resolved = _resolveUniqueName( baseName, (name) => existingFiles.contains('$name.png'), ); return '$resolved.png'; } /// 定量モード撮影用のファイル名 prefix を生成する. /// /// 形式: `MiniTIAS_QM_YYYYMMDD_HHmmss` /// 同秒の重複がある場合は `_1`, `_2` ...と連番を付与する. /// 重複判定は `.dng` の存在チェックで行う. Future generateQuantitativeBaseName() async { final baseName = 'MiniTIAS_QM_${_buildTimestamp(DateTime.now())}'; return _resolveUniqueNameAsync( baseName, (name) => File('$directoryPath/$name.dng').exists(), ); } /// テスト用同期版.既存ファイル名のリストとタイムスタンプを受け取って baseName を返す. /// /// 候補 `MiniTIAS_QM_{timestamp}` に対して `.dng` の存在チェックを行う. static String generateQuantitativeBaseNameSync( String timestamp, List existingFiles, ) { final baseName = 'MiniTIAS_QM_$timestamp'; return _resolveUniqueName( baseName, (name) => existingFiles.contains('$name.dng'), ); } /// ライブプレビューのスナップショット JPEG を `.preview.jpg` として保存する. /// /// [previewBytes] は JPEG エンコード済みのバイト列. /// 戻り値は保存したファイルのフルパス. Future savePreviewJpeg( String baseName, Uint8List previewBytes, ) async { await _ensureDirectory(); final filePath = '$directoryPath/$baseName.preview.jpg'; await File(filePath).writeAsBytes(previewBytes); return filePath; } /// メタデータと LSC マップを `.meta.json` として保存する. /// /// [lscMap] が null の場合,`lscMap`/`lscMapRowCount`/`lscMapColumnCount` キーは含めない. Future saveQuantitativeMetadata( String baseName, Map metadata, { List>? lscMap, int? lscMapRowCount, int? lscMapColumnCount, }) async { await _ensureDirectory(); final Map jsonMap = Map.from(metadata); if (lscMap != null) { jsonMap['lscMap'] = lscMap; jsonMap['lscMapRowCount'] = lscMapRowCount; jsonMap['lscMapColumnCount'] = lscMapColumnCount; } const encoder = JsonEncoder.withIndent(' '); final filePath = '$directoryPath/$baseName.meta.json'; await File(filePath).writeAsString(encoder.convert(jsonMap)); return filePath; } }