Newer
Older
TIASShot / TIASshot / Utils / IoUtil.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Media;
using OpenCvSharp;

namespace TIASshot {
    /// <summary>
    /// ファイル I/O(CSV 読み書き・保存フォルダ生成・撮影情報出力)とサウンド再生の
    /// ユーティリティ.CameraBase から委譲される.状態を持たない.
    /// </summary>
    internal static class IoUtil {

        /// <summary>
        /// csvファイルからMatを読み込む
        /// </summary>
        /// <param name="csvFile"></param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        public static Mat LoadMatFromCsv(string csvFile) {
            try {
                var arr = new List<double[]>();
                int cols = -1;
                using (var reader = new StreamReader(csvFile)) {
                    while (!reader.EndOfStream) {
                        var line = reader.ReadLine();
                        var valStrs = line.Split(',');
                        if (cols == -1) cols = valStrs.Length;
                        else if (cols != valStrs.Length) throw new Exception("cols != valStrs.Length");
                        // CSV はピリオド小数で記述される仕様のため,OS ロケールに依存せず解釈する
                        var vals = valStrs.Select(
                            x => double.Parse(x, System.Globalization.CultureInfo.InvariantCulture)).ToArray();
                        arr.Add(vals);
                    }
                }
                var m = new Mat(arr.Count, cols, MatType.CV_64FC1);
                for (var row = 0; row < arr.Count; row++) {
                    for (var col = 0; col < cols; col++) {
                        m.At<double>(row, col) = arr[row][col];
                    }
                }
                return m;
            } catch (Exception) {
                return null;
            }
        }

        /// <summary>
        /// Matをcsvファイルに保存
        /// </summary>
        /// <param name="csvFile"></param>
        /// <param name="m"></param>
        public static void SaveMatToCsv(string csvFile, Mat m) {
            using (var writer = new StreamWriter(csvFile)) {
                for (var row = 0; row < m.Rows; row++) {
                    var line = "";
                    for (var col = 0; col < m.Cols; col++) {
                        if (col > 0) line += ",";
                        // カンマ小数ロケールでは小数点が列区切りと衝突するため,常にピリオド小数で出力する
                        line += m.At<double>(row, col).ToString(
                            "0.0000000", System.Globalization.CultureInfo.InvariantCulture);
                    }
                    writer.WriteLine(line);
                }
            }
        }

        /// <summary>
        /// 保存フォルダを生成し,そのパスを返す
        /// </summary>
        /// <param name="note"></param>
        /// <returns>生成した保存フォルダのパス</returns>
        public static string CreateSaveFolder(string note) {
            var dt = DateTime.Now;
            var paths = new List<string>() {
                Config.GetString("File/SaveFolder"),
                dt.ToString("yyyy-MM-dd"),
                dt.ToString("HH_mm_ss") + $"-{SanitizeFolderNameElement(note)}",
            };
            var saveFolder = Path.Combine(paths.ToArray());
            Directory.CreateDirectory(saveFolder);
            return saveFolder;
        }

        /// <summary>
        /// ユーザー入力をフォルダ名の一要素として安全に使えるよう無害化する.
        /// パス区切り(\ /)・ドライブ区切り(:)を含む不正なファイル名文字を _ に置換し,
        /// 保存先がベースフォルダの外へ出る(パストラバーサル)・例外で撮影が中断することを防ぐ.
        /// </summary>
        /// <param name="name">ユーザー入力の名前(null 可)</param>
        /// <returns>不正文字を _ に置換した文字列</returns>
        internal static string SanitizeFolderNameElement(string name) {
            if (string.IsNullOrEmpty(name)) return "";
            var invalidChars = Path.GetInvalidFileNameChars();
            var chars = name.ToCharArray();
            for (var i = 0; i < chars.Length; i++) {
                if (Array.IndexOf(invalidChars, chars[i]) >= 0) chars[i] = '_';
            }
            return new string(chars);
        }

        /// <summary>
        /// 設定ファイル由来のファイル名を保存フォルダと結合し,保存先ファイルパスを返す.
        /// ファイル名に含まれる不正なファイル名文字(パス区切り \ /・ドライブ区切り : 等)を
        /// _ に置換してから結合するため,設定の破損・誤設定(絶対パス指定・..\ トラバーサル)でも
        /// 保存先が保存フォルダの外へ出ない.同梱 config.xml の既存値では従来の
        /// Path.Combine と完全に同一の結果を返す(挙動不変).
        /// </summary>
        /// <param name="saveFolder">保存フォルダのパス</param>
        /// <param name="filename">設定ファイル由来のファイル名({CN}/{NO} 置換後)</param>
        /// <returns>保存先ファイルパス(必ず saveFolder 配下)</returns>
        public static string CombineSaveFilePath(string saveFolder, string filename) {
            return Path.Combine(saveFolder, SanitizeFolderNameElement(filename));
        }

        /// <summary>
        /// 画像をファイルに保存する.
        /// Cv2.ImWrite は保存失敗(ディスク満杯・パス不正・権限等)で例外を出さず false を
        /// 返すだけのため,戻り値を検査して失敗を Debug.WriteLine に記録し bool で返す.
        /// 例外(拡張子不正等)は捕捉せず伝播する(従来挙動どおり).
        /// </summary>
        /// <param name="path">保存先ファイルパス</param>
        /// <param name="img">保存する画像</param>
        /// <returns>保存に成功したら true</returns>
        public static bool SaveImage(string path, Mat img) {
            var ok = Cv2.ImWrite(path, img);
            if (!ok) Debug.WriteLine($"画像の保存に失敗しました: {path}");
            return ok;
        }

        /// <summary>
        /// 設定値をCSVファイルに書き込む.
        /// 値(ユーザー入力のデータ名・保存フォルダのパス等)にカンマ・ダブルクォート・改行が
        /// 含まれても行が崩れないよう,RFC 4180 に従ってクォートする(該当しない値は従来どおり).
        /// </summary>
        /// <param name="sw"></param>
        /// <param name="info"></param>
        public static void WriteInfo(StreamWriter sw, Dictionary<string, string> info) {
            sw.WriteLine($"撮影日時,{DateTime.Now:yyyy/MM/dd HH:mm:ss}"); // 撮影日時を追加
            foreach (var kvp in info) {
                sw.WriteLine($"{EscapeCsvField(kvp.Key)},{EscapeCsvField(kvp.Value)}");
            }
        }

        /// <summary>
        /// CSV の 1 フィールドを RFC 4180 に従ってエスケープする.
        /// カンマ・ダブルクォート・CR・LF を含む場合のみ全体をダブルクォートで囲み,
        /// 内部のダブルクォートを二重化する.それ以外はそのまま返す(null は空文字).
        /// WriteInfo のほか,撮影ログ(CameraBase.CaptureShots の Note 欄)でも使う.
        /// </summary>
        /// <param name="field">フィールド値(null 可)</param>
        /// <returns>エスケープ済みのフィールド</returns>
        internal static string EscapeCsvField(string field) {
            if (string.IsNullOrEmpty(field)) return "";
            if (field.IndexOfAny(new[] { ',', '"', '\r', '\n' }) < 0) return field;
            return "\"" + field.Replace("\"", "\"\"") + "\"";
        }

        /// <summary>
        /// 効果音を再生する
        /// </summary>
        /// <param name="wavfile"></param>
        public static void PlaySound(string wavfile) {
            if (!File.Exists(wavfile)) {
                Debug.WriteLine($"音声ファイルが見つかりません: {wavfile}");
                return;
            }
            try {
                using (var player = new SoundPlayer(wavfile)) {
                    player.Play();
                }
            } catch (Exception ex) {
                Debug.WriteLine($"音声再生に失敗しました: {ex.Message}");
            }
        }
    }
}