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; } /// ファイル名を生成する. /// /// 形式: MiniTIAS_YYYYMMDD_HHmmss.png /// 同秒の重複がある場合は _1, _2, ... と連番を付与する. Future generateFileName() async { final timestamp = _buildTimestamp(DateTime.now()); final baseName = 'MiniTIAS_$timestamp'; final candidate = '$baseName.png'; if (!await File('$directoryPath/$candidate').exists()) { return candidate; } var suffix = 1; while (await File('$directoryPath/${baseName}_$suffix.png').exists()) { suffix++; } return '${baseName}_$suffix.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 candidate = '$baseName.png'; if (!existingFiles.contains(candidate)) { return candidate; } var suffix = 1; while (existingFiles.contains('${baseName}_$suffix.png')) { suffix++; } return '${baseName}_$suffix.png'; } /// 定量モード撮影用のファイル名 prefix を生成する. /// /// 形式: `MiniTIAS_QM_YYYYMMDD_HHmmss` /// 同秒の重複がある場合は `_1`, `_2` ...と連番を付与する. /// 重複判定は `.dng` の存在チェックで行う. Future generateQuantitativeBaseName() async { final candidate = 'MiniTIAS_QM_${_buildTimestamp(DateTime.now())}'; if (!await File('$directoryPath/$candidate.dng').exists()) { return candidate; } var suffix = 1; while (await File('$directoryPath/${candidate}_$suffix.dng').exists()) { suffix++; } return '${candidate}_$suffix'; } /// テスト用同期版.既存ファイル名のリストとタイムスタンプを受け取って baseName を返す. /// /// 候補 `MiniTIAS_QM_{timestamp}` に対して `.dng` の存在チェックを行う. static String generateQuantitativeBaseNameSync( String timestamp, List existingFiles, ) { final candidate = 'MiniTIAS_QM_$timestamp'; if (!existingFiles.contains('$candidate.dng')) { return candidate; } var suffix = 1; while (existingFiles.contains('${candidate}_$suffix.dng')) { suffix++; } return '${candidate}_$suffix'; } /// ライブプレビューのスナップショット 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; } }