using System;
using System.IO;
using System.Collections.Generic; // List<>
using System.Linq; // for Enumration

using System.Text;
using System.Threading;
using System.Threading.Tasks;

using System.Text.RegularExpressions;
using System.Security.Cryptography;        // for Hash
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Collections.ObjectModel;
using System.Runtime.InteropServices; // for DLL import


// PDFiumの追加
using PdfiumViewer;

// no need
//using System.Windows; // No need
//using System.Windows.Media.Imaging; // no need
// for Stream conv.
//using System.Runtime.InteropServices.WindowsRuntime;
//using Windows.Storage.Streams;
//using System.Reflection;                // for Assembly.

using static CSRender.RenderPDF;

namespace CSRender {
#pragma warning disable IDE1006 // 小文字のメソッドを許可
	static public class Check
	{
		//https://gist.github.com/retorillo/4e0c4a3cf4c7096e05ac

		static public bool bDump = false;
		static public bool bThDump = false;

	    [DllImport("user32.dll")]
		extern static bool SetProcessDPIAware();
		[DllImport("user32.dll")]
		extern static IntPtr GetWindowDC(IntPtr hwnd);
		[DllImport("gdi32.dll")]
		extern static int GetDeviceCaps(IntPtr hdc, int index);
		[DllImport("user32.dll")]
		extern static int ReleaseDC(IntPtr hwnd, IntPtr hdc);

        public static int GetDPI() {
            // モニタの解像度の取得
			//return 96;
			SetProcessDPIAware();// もしくはdpiAwareをマニフェストで設定すればよい。
			var dc = GetWindowDC(IntPtr.Zero);
			var dpi = GetDeviceCaps(dc, 88/*LOGPIXELSX*/);
			ReleaseDC(IntPtr.Zero, dc);
			return dpi;// 96;// dpi;
		}
	}

	public static class RenderPDF
	{
		/// <summary>
		/// Rendering Condition
		/// </summary>
		[DataContract]
		public class RenderConditionParams
		{
			[DataMember(Order = 0)]
			public double	Dpi			= 72.0;
			[DataMember(Order = 1)]
			public string	BoxType		= "Crop";
			[DataMember(Order = 2)]
			public string	ImageType	= "JPEG";
			[DataMember(Order = 3)]
			public int		JpegQ		= 91;
			// Method non
		}

        /*
         *  Console Echo関係
         */
		public static bool bEcho = false;
        public static void setEcho(bool b) {bEcho = b;}
		public static void echo(params object[] args) {
			if (!bEcho) return;
			var s = "";
			foreach (object a in args) {
				s += a.ToString();
			}
			Console.WriteLine(s);
		}

		/// <summary>
		/// PDFのレンダリング
		/// </summary>
		/// <param name="pdfPath"></param>
		/// <param name="inDir"></param>
		/// <param name="pm">レンダリングの条件</param>
		/// <param name="inPageRange"></param>
		/// <param name="inMode"></param>
		/// <param name="bSaveImage"></param>
		/// <param name="bHash">未使用,常にtrueで使用</param>
		/// <param name="inHashData"></param>
		/// <returns></returns>
		public static int RenderPdfDoc(
			string pdfPath,
			string inDir = "",
			RenderConditionParams pm = null,
			string inPageRange = "1-*",
			string inMode = "pageBitMap",
			bool bSaveImage = true,         //ハッシュ値のみ計算するときにfalseにする
			bool bHash = false,
			UtHash.HashData inHashData = null,
			bool bPDFium = false,
			int nPageThreadNum = 4
		) {
			if (pm == null) {
				pm = new RenderConditionParams();
			}
			var otDir = inDir;
			var otBaseName = Path.GetFileName(pdfPath);//*.pdf
			var otExtention = pm.ImageType.ToLower();
			if (bSaveImage) {
				if ( otDir == "" ) {
					// 出力ディレクトリが空→元PDFの同一フォルダにIMGフォルダを作成する
					otDir = Path.Combine(Directory.GetParent(pdfPath).FullName, "IMG");
				}
				if (!Directory.Exists(otDir))
					Directory.CreateDirectory(otDir);
				echo("ouptput dir=",otDir);
			}
            //  Open pdf by PDFium.
			int pageCount = 0;
			using(var docG = PdfiumViewer.PdfDocument.Load(pdfPath)){
				pageCount = docG.PageCount;
				inMode = "pageBitMap";
				var taskList = new List<Task<int>>();
				uint[] rNo = makePageRange(inPageRange, (uint)pageCount);

				if (inHashData != null) {
					echo("Create Hash");
					// ハッシュ対象の最大ページ数の設定必須。
					inHashData.SetFile(pdfPath, (int)pageCount);
				}

				echo($"Page.ParallelOptions:Thread={nPageThreadNum},Pages={rNo.Length}");
				/////並行処理するスレッド数を指定（2-4ぐらいが穏便な数値）
				ParallelOptions options = new ParallelOptions { MaxDegreeOfParallelism = nPageThreadNum };
				Parallel.ForEach(rNo, options, i => {
					//Console.WriteLine($"*****{i}*****/{rNo.Length}");
					var hashValue = "";
					var ret = RenderPage(
							docG: docG,
							index: (int)i-1,
							otPath: Path.Combine(otDir, $"{otBaseName}.{i}.{otExtention}"),
							pm: pm,
							bSaveImage: bSaveImage, // ファイル保存
							bHash: bHash,
							otHashCode: ref hashValue
					);
					if (inHashData != null) {
						lock (inHashData) {
							inHashData.AddHashCode(pdfPath, (int)i, hashValue);
						}
					}
				});
				echo("End Para");
				docG.Dispose();
			}
			return pageCount;// ページ数を返却します
		}
		/// <summary>
		/// imageをRGB３チェンネルbitmapにRenderingする
		/// </summary>
		/// <returns></returns>
		public static  System.Drawing.Bitmap RenderRgbBitMap(
				PdfiumViewer.PdfDocument docG,
				int index,
				double dpiX,
				double dpiY,
				PdfRenderFlags flg 
			){
			var bitmap = docG.Render(index, (float)dpiX, (float)dpiY, flg) as System.Drawing.Bitmap;
			if (bitmap.PixelFormat == System.Drawing.Imaging.PixelFormat.Format24bppRgb) 
				return bitmap;
			//
			var cloneRect = new System.Drawing.RectangleF(0, 0, bitmap.Size.Width, bitmap.Size.Height);
			var format = System.Drawing.Imaging.PixelFormat.Format24bppRgb;
			var clonebitmap = bitmap.Clone(cloneRect, format);
			//
			//bitmap.Dispose();
			return clonebitmap;
			/*
				保存時にBitmapのクローンを作成し、その際のBitmapの形式に24bitRGB(PixelFormat.Format24bppRgb)を
				指定し3版RGBTiffを出力します。
				System.Drawing.Imaging.PixelFormat format = PixelFormat.Format24bppRgb;
				clonebitmap = bitmap1.Clone(cloneRect, format);
				ref:		
				https://maywork.net/computer/csharp_convert_to_format32bppargb/
				https://www.webdevqa.jp.net/ja/c%23/c%EF%BC%83%E3%81%A7%E3%81%AE%E3%83%93%E3%83%83%E3%83%88%E3%83%9E%E3%83%83%E3%83%97pixelformats%E3%81%AE%E5%A4%89%E6%8F%9B/968763414/
				https://gist.github.com/nissuk/888601/ee0943dd0d35dc9c6b47358b9e1a89af8cc9898b
				https://stackoverflow.com/questions/28448474/render-pdf-page-to-bitmap-using-pdfium
				https://qiita.com/Nuits/items/4a2fbc0f4e8583bd5531
				PixelFormat dump https://maywork.net/computer/csharp_convert_to_format32bppargb/
			*/
		}
		/// <summary>
		/// pdfのページを画像に出力する
		/// </summary>
		/// <param name="doc">PdfDocument</param>
		/// <param name="nPage">ページ番号</param>
		/// <param name="otPath">出力ファイル名</param>
		/// <returns>正常:0 </returns>
		public static int RenderPage(
				PdfiumViewer.PdfDocument docG,
				int index,
				string otPath,
				ref string otHashCode,
				// Optional:
				RenderConditionParams pm =  null,
                bool bSaveImage = true,// ファイル保存有無。ハッシュ値計算のみのときにfalseにする
				bool bHash = false
		){
			var bDump = Check.bDump;
			var bThDump = Check.bThDump;
            if ( pm == null )
				pm = new RenderConditionParams();

            using (var memStrm = new MemoryStream()) {
                PdfRenderFlags flg = (PdfRenderFlags.ForPrinting | PdfRenderFlags.CorrectFromDpi);
                using(var img = RenderRgbBitMap(docG, index, (float)pm.Dpi, (float)pm.Dpi, flg) ){
					img.Save(memStrm, System.Drawing.Imaging.ImageFormat.Bmp);
					/*
						// ImageFormatJPEGの時
						dpi = 350,TIFF
						2691.777 = 44分51.777秒
						10335ファイルで20G
						ーー
						ImageFormat.Bmpに変更 
						result=0,time=2850.955[sec]
						10335ファイルで12.2G
						ーー
						pagePara=2
						para=4
						ImageFormat.Bmp
						result=0,time=2824.644[sec]

						10335ファイルで12.2G
						ーー
						pagePara=2
						para=8
						ImageFormat.Bmp
						result=0,time=2828.631[sec] => 47分
						10335ファイルで12.2G

					*/
					// [注意] PDFuim用Flush()必要しないとだめです
					memStrm.Flush();
					img.Dispose();
				}
				var bmp = new System.Drawing.Bitmap(memStrm);
	            if (bDump) {
                    echo($"bmp=w:{bmp.Size.Width},h:{bmp.Size.Height}");
                }
                //Console.WriteLine($"OrgReso({bmp.HorizontalResolution},{bmp.VerticalResolution})");
                if (bHash) {
                    var h = GetHashValue(memStrm);
                    //Console.WriteLine($"HashString:{h}");
                    otHashCode = h;
                }
                var th = Thread.CurrentThread.ManagedThreadId;
                if (!bSaveImage) { //ファイル保存しない.ハッシュ計算のみ
                    if (bThDump) {
                        echo($"ot[th{th},{(index + 1)}/{docG.PageCount}]=,{Path.GetFileName(otPath)},hash:{otHashCode}");
                    }
                    return 0;
                }
				// image encodeを作成
                var imEnc = new UtImage.Enc(pm.ImageType) { JpegQuality = pm.JpegQ };// Init membrers.
                bmp.SetResolution((float)pm.Dpi, (float)pm.Dpi); // imageクラスでもPropertyTagX(Y)Resolutionで設定できそう 
				if (bThDump) {
                    echo($"ot[th{th},{(index + 1)}/{docG.PageCount}]={Path.GetFileName(otPath)}({Directory.GetParent(otPath)})");
                }
                // 
                imEnc.SaveImage(bmp, otPath);
                //
                bmp.Dispose();
				//memStrm.Seek(0);
				memStrm.Dispose();
			}
			return 0;//success
		}

		// Sync version( 比較モード(/FC ) **************************************************************************************

		/// <summary>
		/// レンダリング（比較モード)
		/// </summary>
		/// <param name="pdfPath"></param>
		/// <param name="refPdfPath"></param>
		/// <param name="inDir"></param>
		/// <param name="pm">レンダリング条件</param>
		/// <param name="inPageRange"></param>
		/// <param name="inMode"></param>
		/// <param name="bHash">未使用常にtrueで利用する</param>
		/// <param name="inHashDataTgt"></param>
		/// <param name="inHashDataRef"></param>
		/// <returns>一致した場合は0、不一致の場合は!0を返却する</returns>
		public static int RenderPdfDocCompare(
			string pdfPath,
			string refPdfPath,
			string resultDataPath,
			string inDir = "",
			RenderConditionParams pm = null,
			string inPageRange = "1-*",
			string inMode = "pageBitMap",
			bool bHash = false,
			UtHash.HashData inHashDataTgt = null,
			UtHash.HashData inHashDataRef = null,
			bool bPDFium = false,
			int nPageThreadNum = 4
			// 比較結果を返す ページ番号とメッセージ
		) {
			var bDump = Check.bDump;
			var bThDump = Check.bThDump;

			if(bDump)
				echo("RenderPdfDocCompare & diff Image! & non Para");
			if (pm == null)
				pm = new RenderConditionParams();
			var otDir = inDir;
			var otBaseName = Path.GetFileName(pdfPath);//xxx.pdf
			var otExtention = pm.ImageType.ToLower();
			if (otDir == "") {
				Console.WriteLine("Error:No output dir");
				return -1;// error
			}
			if (!Directory.Exists(otDir))
				Directory.CreateDirectory(otDir);

            // PDFファイルを読み込む by PDFium.
            var docG    = PdfiumViewer.PdfDocument.Load(pdfPath);
            var docGRef = PdfiumViewer.PdfDocument.Load(refPdfPath);

            var pageCount = docG.PageCount;

			uint[] rNo = makePageRange(inPageRange, (uint)pageCount);

			int nRetAll = 0;// トータルの不一致ページ数

			// ハッシュ情報の確認
			/*
				- 双方のハッシュ値が存在するか確認
				- tgtののみなら、refのrender
				- refのみなら、tgtのみrender
				- とう感じ
			 */
			UtHash.HashFile tgtHf=null;
			if (inHashDataTgt!=null) {
				var f = Path.GetFileName(pdfPath);
				if (inHashDataTgt.Files.ContainsKey(f)) {
					tgtHf = inHashDataTgt.Files[f];
				}
			}
			UtHash.HashFile refHf = null;
			if (inHashDataRef != null) {
				var f = Path.GetFileName(refPdfPath);
				if ( inHashDataRef.Files.ContainsKey(f) ) {
					refHf = inHashDataRef.Files[f];
				}
			}
			Console.WriteLine($@"UsingHashFile:tgt({(tgtHf==null?0:1)}),ref({(refHf == null ? 0 : 1)})");
			// tgtHf,refHfを構築完了

			var fcResultMsg = new SortedDictionary<int,string>();// 比較結果を返す ページ番号とメッセージ

			/////並行処理するスレッド数を指定（2-4ぐらいが穏便な数値）
			ParallelOptions options = new ParallelOptions { MaxDegreeOfParallelism = 4 };
			Parallel.ForEach(rNo, options, i => {
				MemoryStream tgtStrm = null, refStrm = null;
				string tgtHash = "", refHash = "";
				// ターゲット レンダラ定義
				MemoryStream RendPageTGT() {
					var strm = RenderPageStream(docG:docG,index:(int)i-1, pm:pm);
					return strm;
				}
				// リファレンス レンダラ定義
                MemoryStream RendPageREF() {
					var strm = RenderPageStream(docG:docGRef, index:(int)i-1, pm:pm);
					return strm;
				}

                // ターゲット処理
				if (tgtHf == null) {
					tgtStrm = RendPageTGT();// Render
					tgtHash = GetHashValue(tgtStrm);
				} else {
					lock (inHashDataTgt) {
						tgtHash = tgtHf.GetHashValue((int)(i - 1));
					}
				}
				// リファレンス処理
				if (refHf == null) {
					refStrm = RendPageREF();// Render
					refHash = GetHashValue(refStrm);
				} else {
					lock (inHashDataRef) {
						refHash = refHf.GetHashValue((int)(i - 1));
					}
				}
				// compare stream;
				// int nRet = 1;//異なる(diff).
				var dateStr = DateTime.Now.ToString();
				//Console.WriteLine($@"tgt={i}:{tgtHash}");
				//Console.WriteLine($@"ref={i}:{refHash}");

				if (tgtHash != refHash) {
                    // no match
					// nRet = 1;
					nRetAll++;// = nRet;//全体の不一致設定
					echo($"{i}:NotMatch:{i},tgt[{tgtHash}],ref[{refHash}]");
					//2020年3月2日 15:23:55:[@Difference]:<fullpath>.diff.jpg
					lock (fcResultMsg) {
						fcResultMsg.Add((int)i, $@"{dateStr}:[@Difference]:{Path.GetFileName(pdfPath)}.{i}");
					}
					echo("***** save diff image");
					// diffのjpegを書き出してみる(tgt)
					if (tgtStrm == null)
                        tgtStrm = RendPageTGT();
					var otPath = Path.Combine(otDir, $"{otBaseName}.{i}.tgt.{otExtention}");
					using(var bmp = new System.Drawing.Bitmap(tgtStrm)){
						var imEnc = new UtImage.Enc(pm.ImageType) { JpegQuality = pm.JpegQ };// Init membrers.
						bmp.SetResolution((float)pm.Dpi, (float)pm.Dpi);
						imEnc.SaveImage(bmp, otPath);
						bmp.Dispose();
					}
					// diffのjpegを書き出してみる(ref)
					if (refStrm == null)
                        refStrm = RendPageREF();
					otPath = Path.Combine(otDir, $"{otBaseName}.{i}.ref.{otExtention}");
					using(var bmp = new System.Drawing.Bitmap(refStrm)){
						var imEnc = new UtImage.Enc(pm.ImageType) { JpegQuality = pm.JpegQ };// Init membrers.
						bmp.SetResolution((float)pm.Dpi, (float)pm.Dpi);
						imEnc.SaveImage(bmp, otPath);
						bmp.Dispose();
					}
				} else {
                    // match
                    //nRet = 0;
					lock (fcResultMsg) {
						fcResultMsg.Add((int)i, $@"{dateStr}:[OK]:{Path.GetFileName(pdfPath)}.{i}");
                        // Ex 2019年12月18日 19:31:38:[OK]:fname.pdf.1.png
                    }
                }
				// dispose:
				if (refStrm != null) refStrm.Dispose();
				if (tgtStrm != null) tgtStrm.Dispose();
			});
			if ( docG != null)
				docG.Dispose();
			if (docGRef != null)
				docGRef.Dispose();

			// まとめて表示
			foreach ( var v  in fcResultMsg){
				Console.WriteLine("@CMP@"+v.Value);
			}
			if (resultDataPath != "") {
				// まとめて書き出し
				var resCont =($"{pdfPath}\n");
				foreach (var v in fcResultMsg) {
					resCont += ("@CMP@" + v.Value) + "\n";
				}
				File.AppendAllText(resultDataPath, resCont);
			}
			return nRetAll;// success. 不一致数を返す
		}

		/// <summary>
		/// 比較モード
		/// </summary>
		/// <param name="docG">ドキュメントハンドル</param>
		/// <param name="index">ページ番号(0-)</param>
		/// <returns>正常:0 </returns>
		public static MemoryStream RenderPageStream(
				PdfiumViewer.PdfDocument docG,
				int index,
			    // Optional:
				RenderConditionParams pm = null
		) {
			var memStrm = new MemoryStream();

			var bDump = Check.bDump;
			var bThDump = Check.bThDump;
			if (pm == null)
				pm = new RenderConditionParams();
            PdfRenderFlags flg = (PdfRenderFlags.ForPrinting | PdfRenderFlags.CorrectFromDpi);
			System.Drawing.Image img =RenderRgbBitMap(docG, index, (float)pm.Dpi, (float)pm.Dpi, flg);
			img.Save(memStrm, System.Drawing.Imaging.ImageFormat.Bmp);
			//img.Save(memStrm, System.Drawing.Imaging.ImageFormat.Jpeg);
			// [注意] PDFuim用にFlush()必要
			memStrm.Flush();
			img.Dispose();
			return memStrm;
		}

		/// <summary>
		/// ページ１から始まるページ範囲の配列を返す
		/// </summary>
		/// <param name="pageRange"></param>
		/// <param name="maxPage"></param>
		/// <returns></returns>
		private static uint[] makePageRange(string pageRange = "1", uint endPage = 100) {
			var range = new SortedSet<uint>();
			var vec = pageRange.Split(',');//カンマで分割
			var reRange = new Regex($@"([\d]+)[\s]*\-[\s]*([\d]*|\*)");// n-m or n- or n-*
			var reOne = new Regex($@"([\d]+)");                     // n
			bool isRange(uint x) => (0 < x && x <= endPage); // 範囲チェック
			void swap<T>(ref T a, ref T b) { T tmp = a; a = b; b = tmp; }
			foreach (var s in vec) {
				//範囲指定
				var m = reRange.Match(s);
				if (m.Success) {
					uint st = uint.Parse(m.Groups[1].Value);
					uint en = 0;
					if (!isRange(st))
						continue;//開始値が範囲外で無効
					uint.TryParse(m.Groups[2].Value, out en);
					if (!isRange(en))
						en = endPage;// 最終値が無効なため最終ページ
					if (st > en) swap(ref st, ref en);
					// 範囲登録
					foreach (var g in Enumerable.Range((int)st, (int)(en - st + 1)))
						range.Add((uint)g);
					continue;
				}
				//ページ指定
				m = reOne.Match(s);
				if (m.Success) {
					var n = uint.Parse(m.Groups[1].Value);
					if (isRange(n))
						range.Add(n);
				}
			}
			return range.ToArray<uint>();
		}
		public static string GetHashValue(MemoryStream strm) {
			var alg = new SHA256CryptoServiceProvider();
            strm.Seek(0, SeekOrigin.Begin);
			var bin = alg.ComputeHash(strm);
			alg.Clear();
			// バイト配列をUTF8エンコードで文字列化
			var hashedText = new StringBuilder();
			foreach (var b in bin) {
				hashedText.AppendFormat("{0:X2}", b);
			}
			return hashedText.ToString();
		}
	}

	/// <summary>
	/// ファイルのハッシュ値のIO
	/// </summary>
	namespace UtHash
	// https://qiita.com/Akasaki/items/dee137b24aea4b7e2bcb
	// http://mokake.hatenablog.com/entry/2017/09/28/234433
	// https://lifetime-engineer.com/csharp-create-json-indent/
	// DataMemberでOrderを指定することで順序を確定。
	// JsonReaderWriterFactory.CreateJsonWriterでindent=trueにすることでjsonの可視性向上
	{
		[DataContract]
		public class HashHead
		{
			public bool Dirty = false;              // 書き換えられたらTrue 保存対象外
			[DataMember(Order = 0)]
			public string HashBaseName;             // 保存時にベース名を設定する
			[DataMember(Order=1)]
			public string HashFilePath;
			[DataMember(Order=2)]
			public string Dpi;
			[DataMember(Order=3)]
			public string Box;
			[DataMember(Order=4)]
			public string ImType;
			[DataMember(Order=5)]
			public string JQ;
			// メソッド
			public HashHead() {
				SetRenderInfo("0", "Bx", "IMx", "xx");
			}
			public HashHead SetRenderInfo(string dpi, string box, string imType, string jq) {
				Dpi = dpi; Box = box; ImType = imType; JQ = jq;

				return this;
			}
			public string GetRenderInfoStr() { return $@"{Dpi}_{Box}_{ImType}_{JQ}"; }

		}
		[DataContract]
		public class HashFile {
			[DataMember(Order=0)]
			public DateTime UpdateTime;       // ファイルの更新日
			[DataMember(Order=1)]
			public string UpdateTimeStr;       // ファイルの更新日(Json目視用)
			[DataMember(Order=2)]
			public List<string> PageHashCode = new List<string>(); // ページ毎のハッシュ値の配列
			// メソッド
			public string GetHashValue(int i) {
				// 範囲チェック
				if ( !(0  <= i && i < PageHashCode.Count) ){
					return "";
				}
				return PageHashCode[i];
			}
		}
		[DataContract]
		public class HashData {
			[DataMember(Order=0)]
			public HashHead Head = new HashHead();
			[DataMember(Order=1)]
			public SortedDictionary<string, HashFile> Files = new SortedDictionary<string, HashFile>();
			// temp value
			string loadedPath;
			// メソッド
			static public string GetHashFileName(string baseName, string dpi, string box, string imType, string jq) {
				var h = new HashHead();
				var f = baseName + "_" + h.SetRenderInfo(dpi, box, imType, jq).GetRenderInfoStr() + ".json";
				return f;
			}
			static public HashData load(string pdfPath/*or Dir*/, string baseName, string dpi, string box, string imType, string jq, ref string otPath) {
				var hashPath = "";
				if (Path.GetExtension(pdfPath).ToLower() == ".pdf") {
					var paPath = Directory.GetParent(Path.GetFullPath(pdfPath)).ToString();
					var hashFName = GetHashFileName(baseName, dpi, box, imType, jq);
					hashPath = Path.Combine(paPath, hashFName);
				} else if (File.GetAttributes(pdfPath).HasFlag(FileAttributes.Directory)) {
					var hashFName = GetHashFileName(baseName, dpi, box, imType, jq);
					hashPath = Path.Combine(pdfPath, hashFName);
				}
				//Console.WriteLine($@"hashPath={hashPath}");
				if (otPath != null) {
					otPath = hashPath;
				}
				var hd = HashData.load(hashPath); //ファイルの存在はこっちに任す
				return hd;
			}
			static public HashData load(string hashPath) {
				if (!File.Exists(hashPath)) {
					echo("not find load hash data file");
					return null;
				}
				DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(HashData));
				using (var rd = new FileStream(hashPath, FileMode.Open,FileAccess.Read,FileShare.ReadWrite)) {
					var a = (HashData)serializer.ReadObject(rd);
					a.Head.Dirty = false;// clear
					a.loadedPath=hashPath;
					return a;
				}
			}
			static public HashData loadFromString(string s) {
				DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(HashData));
				using (var rd = new MemoryStream( Encoding.UTF8.GetBytes(s) )) {
					var a = (HashData)serializer.ReadObject(rd);
					a.Head.Dirty = false;// clear
					return a;
				}
			}
			public override string ToString() {
				//文字列に書き出す
				using (var m = new MemoryStream())
				using (var writer = JsonReaderWriterFactory.CreateJsonWriter(m, Encoding.UTF8, true, true, "  ")) {
					var serializer = new DataContractJsonSerializer(typeof(HashData));
					serializer.WriteObject(writer, this);
					var x=  Encoding.UTF8.GetString(m.ToArray());// 文字列に変換
					return x;
				}
			}
			public bool save(string path) {
				// JSONに変換するデータを作る。
				// 再保存の抑制
				//this.Head.Dirtyがfalseなら更新されていないことになる。
				//またかつ保存パスが同じ this.Head.HashFilePath = path;

				echo($@"HashData.save:{path}");
				this.Head.HashFilePath = Path.GetFullPath(path);
				this.Head.HashBaseName = this.Head.GetRenderInfoStr();
				try {
					using (var fs = new FileStream(path, FileMode.Create))
					using (var writer = JsonReaderWriterFactory.CreateJsonWriter(fs, Encoding.UTF8, true, true, "  ")) {
						var serializer = new DataContractJsonSerializer(typeof(HashData));
						serializer.WriteObject(writer, this);
						return true;
					}
				} catch (Exception e) {
					Console.WriteLine($@"error Save json anything error:{e}");
					return false;
				}
			}
			public bool SetFile(string pathPDF, int pageNum) {
				var fi = new FileInfo(pathPDF);
				var fName = fi.Name;
				HashFile hf;
				if (!Files.ContainsKey(fName)) {
					this.Head.Dirty = true;// 変更あり
					hf = new HashFile() {
						UpdateTime = fi.LastWriteTimeUtc,
						UpdateTimeStr= fi.LastWriteTime.ToString(),
						PageHashCode = new List<string>(pageNum)
					};
					// Console.WriteLine($@"update={hf.UpdateTime}");
					for (var i = 0; i < pageNum; i++) {
						hf.PageHashCode.Add("");
					}
					Files.Add(fName,hf);
				} else {
					// 既に存在、pageNumがかわらないことだけチェック
					var pageNumOrg = Files[fName].PageHashCode.Count;
					if (Files[fName].UpdateTime.ToString() != fi.LastWriteTimeUtc.ToString()) {
						// 日付が一致しなければ更新(ToStringで比較しないとだめです
						this.Head.Dirty = true;// 変更あり
						Files[fName].UpdateTime = fi.LastWriteTimeUtc;
						Files[fName].UpdateTimeStr = fi.LastWriteTime.ToString();
						Files[fName].PageHashCode = new List<string>(pageNum);
						for (var i = 0; i < pageNum; i++) {
							Files[fName].PageHashCode.Add("");
						}
					}
					// ありえないけど、同一日付でページ数が異なる
					if (pageNumOrg != pageNum) {
						Console.WriteLine($@"Mismatch pageNum:setP:{pageNum},DbP:{Files[fName].PageHashCode.Count}");
						this.Head.Dirty = true;// 変更あり
						return false; // Page数が一致しない
					}
				}
				return true;
			}
			public bool AddHashCode(string pathPDF, int pageNo/*1開始*/, string hashCode) {
				//Console.WriteLine("call addHashCode:" + pathPDF + ":" + pageNo);
				var fi = new FileInfo(pathPDF);
				var fName = fi.Name;
				var n = pageNo - 1;//0始まり
				if (!Files.ContainsKey(fName)) {
					this.Head.Dirty = true;// 変更あり
					return false;
				}
				if (Files[fName].PageHashCode[n] != hashCode) {
					this.Head.Dirty = true;// 変更あり
					Files[fName].PageHashCode[n] = hashCode;
				}
				return true;
			}
			public string GetHashCode(string pathPDF, int pageNo/*1開始*/) {
				var fi = new FileInfo(pathPDF);
				var fName = fi.Name;
				var n = pageNo - 1;//0始まり
				if (!Files.ContainsKey(fName)) {
					return "";
				}
				var v = Files[fName].PageHashCode;
				return (n < v.Count) ? v[n] : "";
			}
			public bool IsValidHashFile(string[] fLst){
				// ハッシュファイルとPDFファイルの整合性確認。更新日付のみ確認。正常:true、異常:false
				// 
				if (fLst.Count() != Files.Count )
					return false; // pdfファイル数の不一致

				foreach (var f in Files) {
					var fPath=Path.Combine( Path.GetDirectoryName(loadedPath) ,f.Key);
					var fi = new FileInfo(fPath);
					// Console.WriteLine(fPath+": "+f.Value.UpdateTimeStr);
					// Console.WriteLine(fPath+"->"+fi.LastWriteTime.ToString());
					if ( f.Value.UpdateTimeStr != fi.LastWriteTime.ToString() )	
						return false; // 1つでも異なっていたらすべてのファイルを異常とする
				}
				return true;//正常
			}
		}

	}

	namespace UtImage // Image保存のユーティリティー
	{
		// ref:https://water2litter.net/rum/post/cs_pdf_wpf/
		//using EncType = System.Drawing.Imaging.Encoder;// 別名
		//using EncParamType = System.Drawing.Imaging.EncoderParameter;//別名

		/// <summary>
		/// BitMapのSaveに使うImage Encodeパラメータの設定値の生成と 画像保存
		/// var enc = new UtImage.Enc("jpeg");
		/// //enc.SetImageType("PNG"); // 後で変更できる
		/// //enc.JJpegQuality = 100; JpegのQuarty変更
		/// enc.SaveImage(BitMap,path);
		/// // enc.SaveImage(BitMap,path,"PNG");
		/// </summary>
		/// 
		public class Enc
		{
			// SEE:https://dobon.net/vb/dotnet/graphics/encoderparameters.html
			public Enc(string imageType = "jpeg") { SetImageType(imageType); }
			public long JpegQuality { get; set; } = 91;// 指定しないときの値は91と一致する
			private string ImageType = "jpeg";

			public Enc SetImageType(string imageType) {
				imageType = imageType.ToLower();
				if (imageType == "jpeg" || imageType == "jpg") {
					ImageType = "jpeg";
				} else if (imageType == "png") {
					ImageType = "png";
				} else if (imageType == "tif" || imageType == "tiff") {
					ImageType = "tiff";
				} else {
					ImageType = imageType;// 小文字を設定
				}
				return this;
			}
			public int SaveImage(System.Drawing.Bitmap bitmap, string path, string imageType = null) {
				var p = GetParams();
				string imType = imageType?.ToLower();
				if (p != null) {
					bitmap.Save(path, GetInfo(imType), GetParams());// imageTypeがnullならSetImageType()のものが使われる
				} else {// パラメータが空の場合
					var imageFormat = new System.Drawing.Imaging.ImageFormat(GetInfo(imType).FormatID);
					bitmap.Save(path, imageFormat);
				}
				return 0;
			}
			// Image版
			public int SaveImage(System.Drawing.Image image, string path, string imageType = null)
			{
				var p = GetParams();
				string imType = imageType?.ToLower();
				if (p != null) {
					image.Save(path, GetInfo(imType), GetParams());// imageTypeがnullならSetImageType()のものが使われる
				}
				else
				{// パラメータが空の場合
					var imageFormat = new System.Drawing.Imaging.ImageFormat(GetInfo(imType).FormatID);
					image.Save(path, imageFormat);
				}
				return 0;
			}
			/// <summary>
			/// サポートするEncodeパラメータ一覧
			/// 現状Errorが発生して取得できない
			/// </summary>
			/// <param name="bitmap1">対象のBitMap</param>
			public static void GetSupportedParameters(System.Drawing.Bitmap bitmap1 = null) {
				// https://docs.microsoft.com/ja-jp/dotnet/framework/winforms/advanced/how-to-determine-the-parameters-supported-by-an-encoder
				try {
					if (bitmap1 == null) {
						bitmap1 = new System.Drawing.Bitmap(100, 100);
						var destBitmapData = bitmap1.LockBits(
								new System.Drawing.Rectangle(0, 0, 100, 100),
								System.Drawing.Imaging.ImageLockMode.ReadOnly,
								bitmap1.PixelFormat
						);
						bitmap1.UnlockBits(destBitmapData);
					}
					var jpgEncoder = GetEncoderInfo(System.Drawing.Imaging.ImageFormat.Tiff);
					var paramList = bitmap1.GetEncoderParameterList(jpgEncoder.Clsid);
					var encParams = paramList.Param;
					for (int i = 0; i < encParams.Length; i++) {
						Console.WriteLine("Param " + i + " holds " + encParams[i].NumberOfValues +
							" items of type " +
							encParams[i].ValueType + "\r\n" + "Guid category: " + encParams[i].Encoder.Guid + "\r\n");
					}
				} catch (Exception e) {
					Console.WriteLine($"Ignore error ={e.ToString()}");
				}
			}
			// inner method.
			private System.Drawing.Imaging.EncoderParameters GetParams() {
				var encList = new List<System.Drawing.Imaging.EncoderParameter>();
				//encList.Clear();
				// 今はJPEGパラメータだけ
				encList.Add(new System.Drawing.Imaging.EncoderParameter(System.Drawing.Imaging.Encoder.Quality, JpegQuality));
				//...
				// 結合
				var eps = new System.Drawing.Imaging.EncoderParameters(encList.Count);
				for (var i = 0; i < encList.Count; i++)
					eps.Param[i] = encList[i];
				return eps;
			}
			private System.Drawing.Imaging.ImageCodecInfo GetInfo(string it = null) {
				if (it == null) {// SetImageType()で指定したものを使う
					it = ImageType;
				}
				return GetEncoderInfo($"image/{it}");
			}
			//ImageFormatで指定されたImageCodecInfoを探して返す
			private static System.Drawing.Imaging.ImageCodecInfo GetEncoderInfo(System.Drawing.Imaging.ImageFormat f) {
				var encs = System.Drawing.Imaging.ImageCodecInfo.GetImageEncoders();
				foreach (var enc in encs) {
					if (enc.FormatID == f.Guid)
						return enc;
				}
				return null;
			}
			private static System.Drawing.Imaging.ImageCodecInfo GetEncoderInfo(string mineType) {
				var encs = System.Drawing.Imaging.ImageCodecInfo.GetImageEncoders();
				//指定されたMimeTypeを探して見つかれば返す
				foreach (System.Drawing.Imaging.ImageCodecInfo enc in encs) {
					if (enc.MimeType == mineType)
						return enc;
				}
				return null;
			}
		}

	}
}
