From 6f700b7f5b517327e463e203151cefa78d1e167e Mon Sep 17 00:00:00 2001 From: matsuo Date: Sat, 2 Oct 2021 14:54:00 +0900 Subject: [PATCH] =?UTF-8?q?CSRender=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Suppert paraPage(Page処理スレッド)数 指定 UWPアプリ、Data.pdfを除外 --- CSRender/CSRender.cs | 711 ++++------- CSRender/CSRender.csproj | 3 +- CSRender/Program.cs | 459 ++++--- CSRender/Properties/AssemblyInfo.cs | 11 +- CSRender/{ => Scrap1}/Program_Scrap.cs | 0 CSRender/{ => Scrap1}/Program_Scrap2.cs | 0 CSRender/{ => Scrap1}/Program_Scrap3.cs | 0 .../{ => Scrap1}/Program_Scrap4_leakOK.cs | 0 CSRender/{ => Scrap1}/Program_mg0607.cs | 0 CSRender/Scrap2/CSRender_full.cs | 1132 +++++++++++++++++ CSRender/Scrap2/Program_full.cs | 1046 +++++++++++++++ bin/Release/CSRender.exe | Bin 82432 -> 71168 bytes bin/Release/CSRender.exe.config | 328 +---- 13 files changed, 2656 insertions(+), 1034 deletions(-) rename CSRender/{ => Scrap1}/Program_Scrap.cs (100%) rename CSRender/{ => Scrap1}/Program_Scrap2.cs (100%) rename CSRender/{ => Scrap1}/Program_Scrap3.cs (100%) rename CSRender/{ => Scrap1}/Program_Scrap4_leakOK.cs (100%) rename CSRender/{ => Scrap1}/Program_mg0607.cs (100%) create mode 100644 CSRender/Scrap2/CSRender_full.cs create mode 100644 CSRender/Scrap2/Program_full.cs diff --git a/CSRender/CSRender.cs b/CSRender/CSRender.cs index 02d33d2..25f739e 100644 --- a/CSRender/CSRender.cs +++ b/CSRender/CSRender.cs @@ -1,81 +1,35 @@ using System; using System.IO; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Generic; // List<> +using System.Linq; // for Enumration + using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Windows; -using System.Windows.Media.Imaging; -using System.Reflection; // Assembly. -using System.Text.RegularExpressions; - -using System.Security.Cryptography; +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 -using System.Runtime.InteropServices; - - -// for Stream conv. -using System.Runtime.InteropServices.WindowsRuntime; - -using Windows.Storage.Streams; - -//using System.IO.WindowsRuntimeStreamExtensions; - - -// - -//using Codeplex.Data; // DynamicJson - - -// UWP-APIの使用 -using Windows.Data.Pdf; // PDFiumの追加 using PdfiumViewer; -using static CSRender.RenderPDF; - - -//https://proself2.screen.co.jp/public/OcyIQAHPksNAnE4Bs3BxIPQApnAEmSCIBOxsCZ946Uur -//Screen8080 -//1_A4縦_2×1_両面_可変長レコード.pdf -// TOshiba: -//#region アセンブリ Windows, Version=255.255.255.255, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime -// C:\UserData\GIT_JSH\WinRT\CSRender\CSRender\bin\Debug\Windows.winmd -// WindowsRuntime 1.3 - -//#endregion -// MacBookPro: -// WindowsRuntime 1.4 -//CSRender, Version=1.1.0.0, Culture=neutral, PublicKeyToken=null -// mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089,Ver(4.0.0.0),VerComp(SameMachine) -// System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089,Ver(4.0.0.0),VerComp(SameMachine) -// Windows, Version=255.255.255.255, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime,Ver(255.255.255.255),VerComp(SameMachine) -// System.Runtime.WindowsRuntime, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089,Ver(4.0.0.0),VerComp(SameMachine) -// System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a,Ver(4.0.0.0),VerComp(SameMachine) -// System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089,Ver(4.0.0.0),VerComp(SameMachine) -// System.Runtime.Serialization, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089,Ver(4.0.0.0),VerComp(SameMachine) -// clrVer=4.0.30319.42000 -// 参考: zoomパレメータなど -// https://www.syncfusion.com/kb/8767/how-to-print-pdf-documents-in-xamarin-forms-platform -//var DisplayInformation = Windows.Graphics.Display.DisplayInformation.GetForCurrentView(); -//var dpi = DisplayInformation.LogicalDpi / 96; +// 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. -// https://elesynd.blogspot.com/2018/11/hDpiForm.html -//Program.csでDPIAwareする -//FormクラスのAutoScaleModeをUIを使って"Dpi"に変更する -//それでもズレるコントロールは、FontをUIを使って明示的に指定してみる -// https://qiita.com/felis_silv/items/efee4b1a397b0b95100a -// スケーリング grph.FromImage(bmp)後にスケール +using static CSRender.RenderPDF; namespace CSRender { -#pragma warning disable IDE1006 // 小文字のメソッド含む - +#pragma warning disable IDE1006 // 小文字のメソッドを許可 static public class Check { //https://gist.github.com/retorillo/4e0c4a3cf4c7096e05ac @@ -83,31 +37,7 @@ namespace CSRender { static public bool bDump = false; static public bool bThDump = false; - const int LOGPIXELSX = 88; - const int LOGPIXELSY = 90; - // - // - const int HORZSIZE = 4;//物理画面の幅・高さ(ミリメートル単位) - const int VERTSIZE = 5; - const int HORZRES = 8; //画面の幅・高さ(ピクセル単位) - const int VERZRES = 10; - // - const int RASTERCAPS = 38; - // 返却値のマスク - //1 (RC_BITBLT) ビットマップの転送 - //2 (RC_BANDING) バンド処理のサポートが必要 - //4 (RC_SCALING) スケーリング - //8 (RC_BITMAP64) 64KB より大きいビットマップ - //0x0080 (RC_DI_BITMAP) SetDIBits 関数と GetDIBits 関数 - //0x0100 (RC_PALETTE) デバイスはパレットベースのデバイスである - //0x0200 (RC_DIBTODEV) SetDIBitsToDevice 関数 - //0x0800 (RC_STRETCHBLT) StretchBlt 関数 - //0x1000 (RC_FLOODFILL) 塗りつぶし - //0x2000 (RC_STRETCHDIB) StretchDIBits 関数 - const int SCALINGFACTORX = 114;//x 軸・ y 軸のスケーリングファクター - const int SCALINGFACTORY = 115; - - [DllImport("user32.dll")] + [DllImport("user32.dll")] extern static bool SetProcessDPIAware(); [DllImport("user32.dll")] extern static IntPtr GetWindowDC(IntPtr hwnd); @@ -116,38 +46,17 @@ namespace CSRender { [DllImport("user32.dll")] extern static int ReleaseDC(IntPtr hwnd, IntPtr hdc); - public static int GetDPI() { + public static int GetDPI() { + // モニタの解像度の取得 //return 96; SetProcessDPIAware();// もしくはdpiAwareをマニフェストで設定すればよい。 var dc = GetWindowDC(IntPtr.Zero); - var dpi = GetDeviceCaps(dc, LOGPIXELSX); - if ( bDump ) { - Console.WriteLine("DpiX: {0}", GetDeviceCaps(dc, LOGPIXELSX)); - Console.WriteLine("DpiY: {0}", GetDeviceCaps(dc, LOGPIXELSY)); - - Console.WriteLine("SCALINGFACTORYX: {0}", GetDeviceCaps(dc, SCALINGFACTORX)); - Console.WriteLine("SCALINGFACTORY: {0}", GetDeviceCaps(dc, SCALINGFACTORY)); - //if ( false ) {// これは倍率で変更されない - // int width = System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width; // 幅(pixel) - // int height = System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height; // 高さ(pixel) - // // 取得したスクリーンのサイズをコンソールに出力する - // string message = string.Format("This screen size is {0} x {1} (pixel)", width, height); - // System.Console.WriteLine(message); - //} - - //if (false){// コンソールアプリでは呼び出せない - // var di = Windows.Graphics.Display.DisplayInformation.GetForCurrentView(); - // Console.WriteLine($@"di.LogicalDpi: {di.LogicalDpi}"); - // Console.WriteLine($@"di.RawDpiX: {di.RawDpiX}"); - // Console.WriteLine($@"di.ResoSacle: {di.ResolutionScale.ToString()}"); - // Console.WriteLine($@"di.StereoEnabled: {di.StereoEnabled}"); - // Console.WriteLine($@"di: {di.ToString()}"); - //} - } + var dpi = GetDeviceCaps(dc, 88/*LOGPIXELSX*/); ReleaseDC(IntPtr.Zero, dc); return dpi;// 96;// dpi; } } + public static class RenderPDF { /// @@ -167,11 +76,11 @@ namespace CSRender { // Method non } - // Verbose - public static void setEcho(bool b) { - bEcho = b; - } + /* + * 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 = ""; @@ -181,59 +90,6 @@ namespace CSRender { Console.WriteLine(s); } - // 事前チェック BashHash - public static bool RenderPdfInitCheck( Stream stream /* PDFストリーム */) { - Windows.Storage.Streams.InMemoryRandomAccessStream rmStrageStream=null; - - {// Stream->Memstream -> byte array -> IBuffer -> InMemmory...Stream() - var ms = new MemoryStream(); - stream.CopyTo(ms); - var bAray = ms.ToArray(); - IBuffer ib = bAray.AsBuffer(); - rmStrageStream = new Windows.Storage.Streams.InMemoryRandomAccessStream(); - //await s.WriteAsync(ib); - async Task wa() => await rmStrageStream.WriteAsync(ib); - var wa_ret = wa().Result; - } - async Task LoadS() => await Windows.Data.Pdf.PdfDocument.LoadFromStreamAsync(rmStrageStream); - var pdfDoc = LoadS().Result; - if (pdfDoc == null) { - Console.WriteLine("error: get PdfDocument failed"); - return false; - } - - // var memStream = new MemoryStream(); - // memStream.AsInputStream(); - // WindowsRuntimeStreamExtensions. - // var ras = WindowsRuntimeStreamExtensions.AsRandomAccessStream(stream); - //https://blog.ch3cooh.jp/entry/20131207/1386342000 - //https://csharp.hotexamples.com/site/file?hash=0xe951daae5be57f9f35507c45eb09a1085819b0a42b846aaa211ac138a2d1b5af&fullName=src/Nutrition/Nutrition.WP/WPOCRService.cs&project=SamirHafez/Nutrition - - //なんだけどな。。。→ .NetFrameworkにAsRandomAccessStream(x)が存在しない。.Net Coreのみみたい。 - - // exeと同一場所に"baseRender.jpg" - string otPath = Path.Combine(Directory.GetParent(Assembly.GetEntryAssembly().Location).FullName,"baseRender.jpg"); - bool bSaveImage = false;// Debug時にtrueにする - Console.WriteLine($"mode=Hash base check"); - var taskList = new List>(); - var hashValue = ""; - - var pm = new RenderConditionParams(); - var ret = RenderPage( - doc: pdfDoc, - page: pdfDoc.GetPage(0), - otPath: otPath, - pm:pm, - bSaveImage: bSaveImage, - bHash: true, - otHashCode: ref hashValue - ); - Console.WriteLine($@"Hash={hashValue},Host={Environment.MachineName},{Environment.OSVersion}"); - return true;// true:OK, false:NG - } - - // Sync version( no async -> Pallale Task ) ************************************************************************************** - /// /// PDFのレンダリング /// @@ -255,7 +111,8 @@ namespace CSRender { bool bSaveImage = true, //ハッシュ値のみ計算するときにfalseにする bool bHash = false, UtHash.HashData inHashData = null, - bool bPDFium = false + bool bPDFium = false, + int nPageThreadNum = 4 ) { if (pm == null) { pm = new RenderConditionParams(); @@ -263,79 +120,54 @@ namespace CSRender { var otDir = inDir; var otBaseName = Path.GetFileName(pdfPath);//*.pdf var otExtention = pm.ImageType.ToLower(); - if (bSaveImage && otDir == "") { - // 出力ディレクトリが空→元PDFの同一フォルダにIMGフォルダを作成する - otDir = Path.Combine(Directory.GetParent(pdfPath).FullName, "IMG"); + 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); } - // PDFファイルを読み込む - async Task GetStrageFilePath(string path) - => await Windows.Storage.StorageFile.GetFileFromPathAsync(path); - var file = GetStrageFilePath(pdfPath).Result; - - - async Task Load(Windows.Storage.StorageFile path) - => await Windows.Data.Pdf.PdfDocument.LoadFromFileAsync(file); - var pdfDoc = Load(file).Result; - - // 2つを合わせることができそう - //async Task GetLoadFromPath(string path) - //{ - // var a1 = await Windows.Storage.StorageFile.GetFileFromPathAsync(path); - // var a2 = await Windows.Data.Pdf.PdfDocument.LoadFromFileAsync(a1); - // return a2; - //}; - //var pdfDoc2 = GetLoadFromPath(pdfPath).Result; - - - if (pdfDoc == null) { - Console.WriteLine("error: get PdfDocument failed"); - return -1; - } - var n = pdfDoc.PageCount; - inMode = "pageBitMap"; - //Console.WriteLine($"mode={inMode},bPDFium={bPDFium}"); - var taskList = new List>(); - uint[] rNo = makePageRange(inPageRange, pdfDoc.PageCount); - - if (inHashData != null) { - // ハッシュ対象の最大ページ数の設定必須。 - inHashData.SetFile(pdfPath, (int)pdfDoc.PageCount); - } - // PDFiumViewer - PdfiumViewer.PdfDocument docG = null; - if (bPDFium) { - docG = PdfiumViewer.PdfDocument.Load(pdfPath); - } - //Console.WriteLine($@"dpi={pm.Dpi}"); + // Open pdf by PDFium. + int pageCount = 0; + using(var docG = PdfiumViewer.PdfDocument.Load(pdfPath)){ + pageCount = docG.PageCount; + inMode = "pageBitMap"; + var taskList = new List>(); + uint[] rNo = makePageRange(inPageRange, (uint)pageCount); - /////並行処理するスレッド数を指定(2-4ぐらいが穏便な数値) - ParallelOptions options = new ParallelOptions { MaxDegreeOfParallelism = 4 }; - Parallel.ForEach(rNo, options, i => { - //Console.WriteLine($"*****{i}*****/{rNo.Length}"); - var hashValue = ""; - var ret = RenderPage( - doc: pdfDoc, - page: pdfDoc.GetPage((uint)(i - 1)), - otPath: Path.Combine(otDir, $"{otBaseName}.{i}.{otExtention}"), - pm: pm, - bSaveImage: bSaveImage, // ファイル保存 - bHash: bHash, - otHashCode: ref hashValue, - docG : docG - ); if (inHashData != null) { - lock (inHashData) { - inHashData.AddHashCode(pdfPath, (int)i, hashValue); + 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); + } } - } - }); - if ( docG != null) + }); + echo("End Para"); docG.Dispose(); - - return (int)pdfDoc.PageCount;// ページ数を返却します - //return 0;// success. + } + return pageCount;// ページ数を返却します } /// /// pdfのページを画像に出力する @@ -345,113 +177,82 @@ namespace CSRender { /// 出力ファイル名 /// 正常:0 public static int RenderPage( - // Need: - Windows.Data.Pdf.PdfDocument doc, // Need GetPage() only. (Total page number); - PdfPage page, // Target Page Data(page.index:0-x) + PdfiumViewer.PdfDocument docG, + int index, string otPath, ref string otHashCode, // Optional: - RenderConditionParams pm = null, - bool bSaveImage = true,// ファイル保存有無。ハッシュ値計算のみのときにfalseにする - bool bHash = false, - PdfiumViewer.PdfDocument docG = null // + RenderConditionParams pm = null, + bool bSaveImage = true,// ファイル保存有無。ハッシュ値計算のみのときにfalseにする + bool bHash = false ){ - if( pm == null) { - pm = new RenderConditionParams(); - } - double dpiWin = Check.GetDPI(); //96.0;/* , dpiPDF = 72.0*/ - //double resoScale = (96.0 / dpiWin); - double resoScale = (pm.Dpi / 96.0); - //double resoScale = (inDpi / dpiWin); - var bDump = Check.bDump; var bThDump = Check.bThDump; + if ( pm == null ) + pm = new RenderConditionParams(); - var opt = new PdfPageRenderOptions() { - // SEE:https://github.com/microsoft/Windows-universal-samples/blob/master/Samples/PdfDocument/cs/Scenario1_Render.xaml.cs - //BitmapEncoderId = Windows.Graphics.Imaging.BitmapEncoder.JpegEncoderId,// JPEG - IsIgnoringHighContrast = false - }; - var boxDic = new Dictionary() { - // Init - ["Media"] = page.Dimensions.MediaBox, - ["Trim"] = page.Dimensions.TrimBox, - ["Bleed"] = page.Dimensions.BleedBox, - ["Crop"] = page.Dimensions.CropBox, - ["Art"] = page.Dimensions.ArtBox - }; - //Dump: - if ( bDump){ - Console.WriteLine($@"PageZoom={page.PreferredZoom}"); - Console.WriteLine($@"PageRotate={page.Rotation.ToString()}"); - - Console.WriteLine($"{"Size",-5}=({page.Size.Width,8:f2},{page.Size.Height,8:f2})"); - foreach (var b in boxDic.Keys) { - var r = boxDic[b]; - Console.WriteLine($"{b,-5}=({r.X,8:f2},{r.Y,8:f2}) W({r.Width,8:f2},{r.Height,8:f2})"); - Console.WriteLine($"{b,-5}=L {r.Left,-8:f2} R {r.Right,-8:f2}) T {r.Top,-8:f2} B {r.Bottom,-8:f2})"); - } - } - - opt.SourceRect = boxDic.ContainsKey(pm.BoxType) ? boxDic[pm.BoxType] : boxDic["Crop"];// Cannot find -> "Crop"; - //Console.WriteLine($"Select {inBoxType} box."); - - // 何も変倍しないとどうなる - opt.DestinationWidth = (uint)(opt.SourceRect.Width * resoScale + 0.5); - opt.DestinationHeight = (uint)(opt.SourceRect.Height * resoScale + 0.5); - - // 150%の時は * 0.5358を設定するとよい解像度になるが、この数値の計算方法がわからない - //opt.DestinationWidth = (uint)(opt.SourceRect.Width * 0.5358/*resoScale*/ + 0.5); - //opt.DestinationHeight = (uint)(opt.SourceRect.Height * 0.5358/*resoScale*/ + 0.5); - - if (bDump) { - Console.WriteLine($"dpi={pm.Dpi},scale={resoScale},width(org)={page.Size.Width}->{opt.DestinationWidth}"); - Console.WriteLine($"dpi={pm.Dpi},scale={resoScale},height(org)={page.Size.Height}->{opt.DestinationHeight}"); - } - using (var memStrm = new Windows.Storage.Streams.InMemoryRandomAccessStream()) { - - if (docG == null) { - async Task RenderToStream(PdfPage p) => await p.RenderToStreamAsync(memStrm, opt); - RenderToStream(page).Wait(); - } else { - PdfRenderFlags flg = (PdfRenderFlags.ForPrinting | PdfRenderFlags.CorrectFromDpi); - System.Drawing.Image img = docG.Render((int)(page.Index), (float)pm.Dpi, (float)pm.Dpi, flg); - img.Save(memStrm.AsStream(), System.Drawing.Imaging.ImageFormat.Jpeg); - // [注意] PDFuim用に直接 AsStream()に書き込んでいるので、AsStream().Flush()しないとだめです - } - - async Task FlushX() => await memStrm.FlushAsync(); - FlushX().Wait(); - - var bmp = new System.Drawing.Bitmap(memStrm.AsStream()); - if (bDump) { - Console.WriteLine($"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 ) { - Console.WriteLine($"ot[th{th},{(page.Index + 1)}/{doc.PageCount}]=,{Path.GetFileName(otPath)},hash:{otHashCode}"); - } - return 0; - } - - var imEnc = new UtImage.Enc(pm.ImageType) { JpegQuality = pm.JpegQ };// Init membrers. - bmp.SetResolution((float)pm.Dpi, (float)pm.Dpi); + using (var memStrm = new MemoryStream()) { + PdfRenderFlags flg = (PdfRenderFlags.ForPrinting | PdfRenderFlags.CorrectFromDpi); + using(var img = docG.Render(index, (float)pm.Dpi, (float)pm.Dpi, flg)){ + img.Save(memStrm, System.Drawing.Imaging.ImageFormat.Bmp); + //img.Save(memStrm, System.Drawing.Imaging.ImageFormat.Jpeg); + /* + // 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) { - Console.WriteLine($"ot[th{th},{(page.Index + 1)}/{doc.PageCount}]={Path.GetFileName(otPath)}({Directory.GetParent(otPath)})"); - } - // - imEnc.SaveImage(bmp, otPath); - // - bmp.Dispose(); + 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 } @@ -482,62 +283,36 @@ namespace CSRender { bool bHash = false, UtHash.HashData inHashDataTgt = null, UtHash.HashData inHashDataRef = null, - bool bPDFium = false + bool bPDFium = false, + int nPageThreadNum = 4 // 比較結果を返す ページ番号とメッセージ ) { - - var bDump = Check.bDump; var bThDump = Check.bThDump; if(bDump) - Console.WriteLine("RenderPdfDocCompare & diff Image! & non Para"); + echo("RenderPdfDocCompare & diff Image! & non Para"); if (pm == null) pm = new RenderConditionParams(); - - var otDir = inDir;// 今は未使用 + var otDir = inDir; var otBaseName = Path.GetFileName(pdfPath);//xxx.pdf var otExtention = pm.ImageType.ToLower(); if (otDir == "") { - // 出力ディレクトリが空→元PDFの同一フォルダにIMGフォルダを作成する - otDir = Path.Combine(Directory.GetParent(pdfPath).FullName, "IMG"); - if (!Directory.Exists(otDir)) - Directory.CreateDirectory(otDir); + Console.WriteLine("Error:No output dir"); + return -1;// error } - // PDFファイルを読み込む - - - - // 以下のコードで良いみたい。 - // [変更待ち] var file2 = Windows.Storage.StorageFile.GetFileFromPathAsync(pdfPath).GetAwaiter().GetResult(); + if (!Directory.Exists(otDir)) + Directory.CreateDirectory(otDir); - async Task GetStrageFilePath(string path) - => await Windows.Storage.StorageFile.GetFileFromPathAsync(path); - var file = GetStrageFilePath(pdfPath).Result; - async Task Load(Windows.Storage.StorageFile path) - => await Windows.Data.Pdf.PdfDocument.LoadFromFileAsync(path); - var pdfDoc = Load(file).Result; - if (pdfDoc == null) { - Console.WriteLine("error: get PdfDocument failed"); - return -1; - } + // PDFファイルを読み込む by PDFium. + var docG = PdfiumViewer.PdfDocument.Load(pdfPath); + var docGRef = PdfiumViewer.PdfDocument.Load(refPdfPath); - // ref - var fileRef = GetStrageFilePath(refPdfPath).Result; - var pdfDocRef = Load(fileRef).Result; - if (pdfDocRef == null) { - Console.WriteLine("error: get PdfDocument(ref) failed"); - return -1; - } + var pageCount = docG.PageCount; - var n = pdfDoc.PageCount; - if ( n != pdfDocRef.PageCount) { - Console.WriteLine($"error: differ page length:{n}:{pdfDocRef.PageCount}"); - return -1; - } - uint[] rNo = makePageRange(inPageRange, n); + uint[] rNo = makePageRange(inPageRange, (uint)pageCount); - int nRetAll = 0;// 一致(def) + int nRetAll = 0;// トータルの不一致ページ数 // ハッシュ情報の確認 /* @@ -565,43 +340,34 @@ namespace CSRender { var fcResultMsg = new SortedDictionary();// 比較結果を返す ページ番号とメッセージ - // PDFiumViewer - PdfiumViewer.PdfDocument docG = null; - PdfiumViewer.PdfDocument docGRef = null; - if (bPDFium) { - docG = PdfiumViewer.PdfDocument.Load(pdfPath); - docGRef = PdfiumViewer.PdfDocument.Load(refPdfPath); - } - - /////並行処理するスレッド数を指定(2-4ぐらいが穏便な数値) ParallelOptions options = new ParallelOptions { MaxDegreeOfParallelism = 4 }; Parallel.ForEach(rNo, options, i => { - Windows.Storage.Streams.InMemoryRandomAccessStream tgtStrm = null, refStrm = null; + MemoryStream tgtStrm = null, refStrm = null; string tgtHash = "", refHash = ""; - // ターゲット レンダー - InMemoryRandomAccessStream RendPageTGT() { - var strm = RenderPageStream(doc:pdfDoc,page:pdfDoc.GetPage((uint)(i-1)), pm:pm,docG:docG); + // ターゲット レンダラ定義 + MemoryStream RendPageTGT() { + var strm = RenderPageStream(docG:docG,index:(int)i-1, pm:pm); return strm; } - // リファレンス レンダー - InMemoryRandomAccessStream RendPageREF() { - var strm = RenderPageStream(doc:pdfDocRef,page:pdfDocRef.GetPage((uint)(i-1)), pm:pm,docG:docGRef); + // リファレンス レンダラ定義 + MemoryStream RendPageREF() { + var strm = RenderPageStream(docG:docGRef, index:(int)i-1, pm:pm); return strm; } - //tgtStrm = RendPageTGT(); + // ターゲット処理 if (tgtHf == null) { - tgtStrm = RendPageTGT(); + tgtStrm = RendPageTGT();// Render tgtHash = GetHashValue(tgtStrm); } else { lock (inHashDataTgt) { tgtHash = tgtHf.GetHashValue((int)(i - 1)); } } - // リファレンス + // リファレンス処理 if (refHf == null) { - refStrm = RendPageREF(); + refStrm = RendPageREF();// Render refHash = GetHashValue(refStrm); } else { lock (inHashDataRef) { @@ -609,52 +375,49 @@ namespace CSRender { } } // compare stream; - int nRet = 1;//異なる(def). + // int nRet = 1;//異なる(diff). var dateStr = DateTime.Now.ToString(); //Console.WriteLine($@"tgt={i}:{tgtHash}"); //Console.WriteLine($@"ref={i}:{refHash}"); if (tgtHash != refHash) { - nRet = 1; - nRetAll = nRet;//全体の不一致設定 + // no match + // nRet = 1; + nRetAll++;// = nRet;//全体の不一致設定 echo($"{i}:NotMatch:{i},tgt[{tgtHash}],ref[{refHash}]"); //2020年3月2日 15:23:55:[@Difference]:.diff.jpg lock (fcResultMsg) { - fcResultMsg.Add((int)i, $@"{dateStr}:[@Difference]:{pdfPath}.diff.jpg"); + fcResultMsg.Add((int)i, $@"{dateStr}:[@Difference]:{Path.GetFileName(pdfPath)}.{i}"); } - - //if ( true && tgtStrm != null && bSave) { - { - echo("***** save diff image"); - // diffのjpegを書き出してみる(tgt) - if (tgtStrm == null) tgtStrm = RendPageTGT(); - var otPath = Path.Combine(otDir, $"{otBaseName}.{i}.tgt.{otExtention}"); - var bmp = new System.Drawing.Bitmap(tgtStrm.AsStream()); + 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(); } - //if (true && refStrm != null && bSave) { - { - // diffのjpegを書き出してみる(ref) - if (refStrm == null) refStrm = RendPageREF(); - var otPath = Path.Combine(otDir, $"{otBaseName}.{i}.ref.{otExtention}"); - var bmp = new System.Drawing.Bitmap(refStrm.AsStream()); + // 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 { - nRet = 0;// match - //Console.WriteLine($"{i}:Match:{i}"); - // 2019年12月18日 19:31:38:[OK]:fname.pdf.1.png + // match + //nRet = 0; lock (fcResultMsg) { - fcResultMsg.Add((int)i, $@"{dateStr}:[OK]:{Path.GetFileName(pdfPath)}.{i}.png"); - } - } + 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(); @@ -676,75 +439,35 @@ namespace CSRender { } File.AppendAllText(resultDataPath, resCont); } - return nRetAll;// success. + return nRetAll;// success. 不一致数を返す } /// /// 比較モード /// - /// PdfDocument - /// ページ番号 - /// 出力ファイル名 + /// ドキュメントハンドル + /// ページ番号(0-) /// 正常:0 - public static Windows.Storage.Streams.InMemoryRandomAccessStream RenderPageStream( - // Need: - Windows.Data.Pdf.PdfDocument doc, // Need GetPage() only. (Total page number); - PdfPage page, // Target Page Data(page.index:0-x) - //string otPath, - // Optional: - RenderConditionParams pm = null, - PdfiumViewer.PdfDocument docG = null // + 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(); - double dpiWin = Check.GetDPI(); //96.0;/* , dpiPDF = 72.0*/ - double resoScale = (pm.Dpi / dpiWin); - var opt = new PdfPageRenderOptions() { - // SEE:https://github.com/microsoft/Windows-universal-samples/blob/master/Samples/PdfDocument/cs/Scenario1_Render.xaml.cs - //BitmapEncoderId = Windows.Graphics.Imaging.BitmapEncoder.JpegEncoderId,// JPEG - IsIgnoringHighContrast = false - }; - // - //async Task PreparePage(PdfPage p) => await p.PreparePageAsync();//ページの完成を待つ - //PreparePage(page).Wait(); - //page.PreparePageAsync - - var boxDic = new Dictionary() { - // Init - ["Media"] = page.Dimensions.MediaBox, - ["Trim"] = page.Dimensions.TrimBox, - ["Bleed"] = page.Dimensions.BleedBox, - ["Crop"] = page.Dimensions.CropBox, - ["Art"] = page.Dimensions.ArtBox - }; - opt.SourceRect = boxDic.ContainsKey(pm.BoxType) ? boxDic[pm.BoxType] : boxDic["Crop"];// Cannot find -> "Crop"; - - opt.DestinationWidth = (uint)(opt.SourceRect.Width * resoScale + 0.5); - opt.DestinationHeight = (uint)(opt.SourceRect.Height * resoScale + 0.5); - if (bDump) - Console.WriteLine($"dpi={pm.Dpi},scale={resoScale},height(org)={page.Size.Height}->{opt.DestinationHeight}"); - var memStrm = new Windows.Storage.Streams.InMemoryRandomAccessStream(); - if(true) { - if (docG == null) { - async Task RenderToStream(PdfPage p) => await p.RenderToStreamAsync(memStrm, opt); - RenderToStream(page).Wait(); - } else { - PdfRenderFlags flg = (PdfRenderFlags.ForPrinting | PdfRenderFlags.CorrectFromDpi); - System.Drawing.Image img = docG.Render((int)(page.Index), (float)pm.Dpi, (float)pm.Dpi, flg); - img.Save(memStrm.AsStream(), System.Drawing.Imaging.ImageFormat.Jpeg); - // [注意] PDFuim用に直接 AsStream()に書き込んでいるので、AsStream().Flush()しないとだめです - memStrm.AsStream().Flush(); - } - async Task FlushX() => await memStrm.FlushAsync(); - //var dmy =FlushX().Result; - FlushX().Wait(); - } - //page.Dispose(); - - return memStrm;// + PdfRenderFlags flg = (PdfRenderFlags.ForPrinting | PdfRenderFlags.CorrectFromDpi); + System.Drawing.Image img = docG.Render(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; } /// @@ -787,10 +510,10 @@ namespace CSRender { } return range.ToArray(); } - public static string GetHashValue(Windows.Storage.Streams.InMemoryRandomAccessStream strm) { + public static string GetHashValue(MemoryStream strm) { var alg = new SHA256CryptoServiceProvider(); - strm.Seek(0); - var bin = alg.ComputeHash(strm.AsStream()); + strm.Seek(0, SeekOrigin.Begin); + var bin = alg.ComputeHash(strm); alg.Clear(); // バイト配列をUTF8エンコードで文字列化 var hashedText = new StringBuilder(); @@ -840,9 +563,7 @@ namespace CSRender { } [DataContract] - public class HashFile - { - + public class HashFile { [DataMember(Order=0)] public DateTime UpdateTime; // ファイルの更新日 [DataMember(Order=1)] @@ -859,14 +580,11 @@ namespace CSRender { } } [DataContract] - public class HashData - { + public class HashData { [DataMember(Order=0)] public HashHead Head = new HashHead(); - [DataMember(Order=1)] public SortedDictionary Files = new SortedDictionary(); - // メソッド static public string GetHashFileName(string baseName, string dpi, string box, string imType, string jq) { var h = new HashHead(); @@ -890,7 +608,6 @@ namespace CSRender { var hd = HashData.load(hashPath); //ファイルの存在はこっちに任す return hd; } - static public HashData load(string hashPath) { if (!File.Exists(hashPath)) { echo("not find load hash data file"); @@ -921,7 +638,6 @@ namespace CSRender { return x; } } - public bool save(string path) { // JSONに変換するデータを作る。 // 再保存の抑制 @@ -954,9 +670,7 @@ namespace CSRender { UpdateTimeStr= fi.LastWriteTime.ToString(), PageHashCode = new List(pageNum) }; - // Console.WriteLine($@"update={hf.UpdateTime}"); - for (var i = 0; i < pageNum; i++) { hf.PageHashCode.Add(""); } @@ -985,7 +699,6 @@ namespace CSRender { } 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始まり @@ -993,7 +706,6 @@ namespace CSRender { this.Head.Dirty = true;// 変更あり return false; } - if (Files[fName].PageHashCode[n] != hashCode) { this.Head.Dirty = true;// 変更あり Files[fName].PageHashCode[n] = hashCode; @@ -1060,6 +772,21 @@ namespace CSRender { } 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; + } /// /// サポートするEncodeパラメータ一覧 /// 現状Errorが発生して取得できない diff --git a/CSRender/CSRender.csproj b/CSRender/CSRender.csproj index 8a1dbe6..0ae266d 100644 --- a/CSRender/CSRender.csproj +++ b/CSRender/CSRender.csproj @@ -44,7 +44,7 @@ false - AnyCPU + x64 pdbonly true ..\bin\Release\ @@ -76,7 +76,6 @@ - False diff --git a/CSRender/Program.cs b/CSRender/Program.cs index 74eb522..8b67106 100644 --- a/CSRender/Program.cs +++ b/CSRender/Program.cs @@ -1,24 +1,19 @@ using System; using System.IO; -using System.Collections.Generic; -using System.Linq; -//using System.Text; -//using System.Threading; -//using System.Threading.Tasks; -//using System.Windows; -//using System.Windows.Media.Imaging; +using System.Collections.Generic; // For List<> +using System.Linq; // For Enumration using System.Reflection; // Assembly. using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using System.Runtime.InteropServices; - using System.Runtime.Serialization; -using System.ServiceModel; // WCF +using System.ServiceModel; // for WCF using System.Diagnostics; // for Process +// no need +//using System.Runtime.InteropServices; // 作成クラス using CSRender; @@ -37,14 +32,16 @@ namespace CSRenderMain { [DataMember] public string pdfPath = ""; //対象ファイル [DataMember] - public string pdfPath2 = ""; //比較ファイル + public string pdfPathRef = ""; //比較ファイル [DataMember] public string outuptImageDir = ""; // /O [DataMember] public string dpi = "72.0"; // /D [DataMember] - public int para = 4; // 並行数 - [DataMember] + public int para = 4; // 並行数(プロセス数) + [DataMember] + public int paraPage = 4; // 並行数(page処理スレッド) + [DataMember] public string boxSelect = "Crop"; // /B [DataMember] public string pageRange = "1-*"; // /P @@ -76,141 +73,113 @@ namespace CSRenderMain { public ParamData Clone() { return (ParamData)MemberwiseClone(); } - } + } public class Program { - static void DispHelp() { - var pgName = Path.GetFileName(Path.GetFileName(Assembly.GetExecutingAssembly().Location));// プログラム名 - var pgNameFullPath = Assembly.GetExecutingAssembly().Location;// プログラム名 - Console.WriteLine("pageNameF="+pgNameFullPath); + static void DispHelpNoArg() { + var asmName = Assembly.GetExecutingAssembly().GetName(); + var pgName = asmName.Name;// "アセンブリ名" + var v1 = asmName.Version; // "1.0.0.0" +// 逐語的文字列リテラル $@, ダブルクウォートは2つでエスケープされる +// HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH + Console.Write( $@" +Version={ v1} +{pgName} [/] :Rendering(to JPEG,TIFF,...) +{pgName} /MkHash [/] :Make Imaged hash keys +{pgName} /FC [/] :Compare files - string msg = - $"{pgName} [/] \n" - + $"* Render of PDF file. available 3 command mode:[Basic Rendering] [Make Hash command] [Compare command], and [Render Options]\n" - + $"\tPDFの画像化は[Basic Rendering]。\n" - + $"\t比較は/MkHash([Make Hash command])後に、/FC([Compare command])で高速実行できます。\n" - + $"\n" - + $"[Basic Rendering] 基本的なレンダリング\n" - + $"\t{pgName} [/] \n" - + $"\t/F : pdfPath(pdfファイル名|ディレクトリ) /Fは省略可能\n" - + $"\t/O : 出力ディレクトリ。省略時は\"IMG\"フォルダ\n" - + $"\n" - + $"[Render Options] レンダリングオプション\n" - + $"\t/D <解像度> : 解像度指定 9 - 300dpi(default=72dpi)\n" - + $"\t/JPG,/JPEG,/PNG,/TIF,/TIFF,/GIF,/BMP: Select one output format.(default=/JPG)\n" - + $"\t/JPEGQ : Jpegの品質指定1-100(default=91)\n" - + $"\n" - + $"\t/P : ページの範囲を指定する(省略時は全ページ)\n" - + $"\t\t連続した範囲を指定する場合は、ハイフン('-')を用いる。終了側を省略すると最終pageまで。\n" - + $"\t\t複数のページを指定する場合は、カンマ(',')を用いる\n" - + $"\t\tEx. /P \"1,2,30-100\" //1,2pages and 30-100pages.\n" - + $"\t[Unsupport] 未対応↓\n" - + $"\t/L : 入力PDFファイルリスト(*unsupport)\n" - + $"\t/T : テンポラリフォルダを指定(省略時は出力先フォルダと同じ(*unsupport no need)\n" - + $"\t/OP <0|1> : オーバープリントのOn/Off (省略時は1)(*unsupport allways on[1])\n" - + $"\t/U <0|1> : 同名上書き設定 0:上書きしない 1:上書き(*unsupport allways overwrite[1])\n" - + $"\t/OFFSET : ミリ単位でオフセットを指定する(省略時は共に0mm)(*unsupport)\n" - + $"\t/PDFium <0or1>: GoogoleのPDFiumViewerエンジンを使用する(default=1>\n" - + $"\t【未】/BM,/BT,/BA,/BA,/BC: Select one box.(default=/BC:CrobBox): Boxies:MediaBox/BleedBox/TrimBox/ArtBox/CropBox\n" +Render of PDF file.available 3 command mode:[Basic Rendering] [Make Hash command] [Compare command], and[Render Options] + PDFの画像化は[Basic Rendering]。 + 比較は/MkHash([Make Hash command]) 後に/FC([Compare command]) で高速実行できます。 +For more information,see /H /? or /? +"); +// HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH + } + static void DispHelpDetail() { + var asmName = Assembly.GetExecutingAssembly().GetName(); + var pgName = asmName.Name;// "アセンブリ名" + var v1 = asmName.Version; // "1.0.0.0" + DispHelpNoArg(); +// 逐語的文字列リテラル $@, ダブルクウォートは2つでエスケープされる +// HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH + Console.Write($@" +[Basic Rendering] 基本的なレンダリング + {pgName} [/] + /F : pdfPath(pdfファイル名|ディレクトリ) /Fは省略可能 + /O : 出力ディレクトリ。省略時は""IMG""フォルダ - + $"\n" - + $"[Make Hash command] 比較用ハッシュ値作成コマンド\n" - + $"\t{pgName} /MkHash ...... \n" - + $"\t/MKHash : ハッシュ値を出力する。前記の[Render Options]を指定すること\n" - + $"\t[HASHファイル作成]\n" +[Render Options] レンダリングオプション + /D <解像度> : 解像度指定 9 - 300dpi(default=72dpi) + /JPG,/JPEG,/PNG,/TIF,/TIFF,/GIF,/BMP: Select one output format.(default=/JPG) + /JPEGQ : Jpegの品質指定1-100(default=91) - + $"\n" - + $"[Compare command] 比較コマンド\n" - + $"\t{pgName} /FC ...... \n" - + $"\t/FC : 2つのPDFを比較する。前記の[Render Options]を指定すること。無名引数が2つ必要です\n" - + $"\t 事前に/MkHashを実行しておくことで高速に処理できる\n" - + $"\n" - + $"\t/RESULT : 比較結果を格納するファイルパス\n" - + $"\t\t/FCコマンドを指定すると一致したら0,不一致なら1を返却するようになる\n" - + $"\t\tは、,<[OK] or [@Difference]>の行で構成される\n" - - + $"\n" - + $"[ELSE ] その他のオプション\n" - + $"\t/PDFium <0|1>:PDFiumライブラリを使う,デフォルト=1\n" - + $"\t/NoExeSepa :実行分離しない(遅い)\n" - + $"\t/para <並行数>:並行数を指定(デフォルト4)\n" - + $"\t内部コマンド:/SubExe :実行分離,PDF単位で別Processで処理\n" - + $"\n" - + $"/H or /? : This help\n" + /P : ページの範囲を指定する(省略時は全ページ) + 連続した範囲を指定する場合は、ハイフン('-')を用いる。終了側を省略すると最終pageまで。 + 複数のページを指定する場合は、カンマ(',')を用いる + Ex. /P ""1,2,30-100"" //1,2pages and 30-100pages. + [Unsupport] 未対応↓ + /L : 入力PDFファイルリスト(*unsupport) + /T : テンポラリフォルダを指定(省略時は出力先フォルダと同じ(*unsupport no need) + /OP <0|1> : オーバープリントのOn/Off (省略時は1)(*unsupport allways on[1]) + /U <0|1> : 同名上書き設定 0:上書きしない 1:上書き(*unsupport allways overwrite[1]) + /OFFSET : ミリ単位でオフセットを指定する(省略時は共に0mm)(*unsupport) + /PDFium <0or1>: GoogoleのPDFiumViewerエンジンを使用する(default=1> + 【未】/BM,/BT,/BA,/BA,/BC: Select one box.(default=/BC:CrobBox): Boxies:MediaBox/BleedBox/TrimBox/ArtBox/CropBox +[Make Hash command] 比較用ハッシュ値作成コマンド + {pgName} /MkHash ...... + /MKHash : ハッシュ値を出力する。前記の[Render Options]を指定すること + [HASHファイル作成] - /* PDFと同じフォルダに - data.<出力条件>.jsonファイルを作成する - 出力条件:+++ - data.72_BT_JPG_Q34.json - 既に存在したら、それを読んでから、追記(もしくは書き換える>。 - */ +[Compare command] 比較コマンド + {pgName} /FC ...... + /FC : 2つのPDFを比較する。前記の[Render Options]を指定すること。無名引数が2つ必要です + 事前に/MkHashを実行しておくことで高速に処理できる + /RESULT : 比較結果を格納するファイルパス + /FCコマンドを指定すると一致したら0,不一致なら1を返却するようになる + は、,<[OK] or [@Difference]>の行で構成される - // Remove TEST - //+$"\t[for TEST]\n" - //+$"\t/M Sync(default) or ASync\n" - //+$"\t/M : pageBitMap(default), pageBitMapImage, page(same as pageBitMap), org(orginal source)\n" - + "\n"; +[ELSE ] その他のオプション + /NoExeSepa :実行分離しない(遅い) + /Para <プロセス並行数>:本Exeの並行数を指定(デフォルト4) + /ParaPage <ページ処理スレッド数>:ページ処理のスレッド数を指定(デフォルト4) - Console.Write(msg); +/H or /? : This help"); +// HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH } -#if COMENT -// 設計の見直し - -* ハッシュ作成のみのコマンド - → /MkHashをつけると ハッシュファイルのみ作成。ただし、ディレクトリ指定に限る - → CSRender /MkHash <各種Renderパラメータ> - -* 単純なRenderを維持。それに同時にハッシュファイルのOn/Off - → /MkHashをつけなければよい。 - → CSRender <各種Renderパラメータ> - -* 比較モードは、Autoハッシュで、存在しなければ最初に作成。Target/Refともに。比較しながら作成してよし - Manualでハッシュなしで、遅いけどできるようにする。ハッシュ動作の検証用に - → /FCをつけると、デフォルトでハッシュ優先で検査(Auto相当)、ハッシュ検査を無効にしたければ - → /NoHash追加でハッシュを無視してTarget/Refとも再計算。ハッシュを使いたければ、事前に/MkHashで - 作成しておけばよい。 -* ヘルプは <単純なRender> <ハッシュ作成> <比較>にわける - * <単純なレンダラ>: 1ファイルおよびディレクトリの比較。 Renderingパラメータの一覧(項目分ける)まで. - * <ハッシュ作成>: /MkHashで高速比較のための前準備。ディレクトリモードでハッシュファイルのみを作成する。Renderingパラメータは合わせること - * <比較>:/FCコマンドで比較の説明 - -* ここにAsiccDocを埋められないの? → 情報なし* -// 遅くなるので -* 比較モード時の差異があったとき、Diffを出す上限値をLimitDiffNumがほしい(デフォルトは各PDFで1pageとしたい) -* 比較NGになって、フルでDIFF画像を出したいときはLimitDiffを解除すべき。単独ファイル指定のとき。 - -// 設計の見直し(END) -#endif /// - /// PDF renderer main + /// PDF render main /// /// - /// + /// 0 正常終了 static int Main(string[] args) { - - // 引数の保持 - var pm = new ParamData(); - - bool bDirMode = false; // ディレクト指定の場合 - - //var pdfPathLst = new List(); + var argsStr = String.Join(@" ", args); + var pm = new ParamData(); // 引数の保持 + bool bDirMode = false; // ディレクト指定の場合 string[] pdfPathLst = null; string[] pdfPathLst2 = null; var pdfPathLstBoth = new List(); var pdfPathLstNoBoth = new List(); - //ハッシュ関係 string hashBaseName = "RenderHash"; - CSRender.Check.GetDPI(); + + // No CSRender.Check.GetDPI(); + + // 未使用 + //{// ベースハッシュ値の計算テスト + // var streamPDF = ResData.GetBaseHashPDF(); + // RenderPDF.RenderPdfInitCheck(streamPDF); + //} - //{// ベースハッシュ値の計算テスト - // // 一旦使用しない - // var streamPDF = ResData.GetBaseHashPDF(); - // RenderPDF.RenderPdfInitCheck(streamPDF); - //} + var logicalProcessNum = Environment.ProcessorCount; - var qu = new Queue(args); // 引数をQueに登録 (qu.Enqueue(a)でも可能) + if ( logicalProcessNum >= 10 ) {// 元々 4(exe)+4(page)なので+2 = プロセス数が10以上の時にexe数を増やす + pm.para = logicalProcessNum - pm.paraPage; + } + /* + * ↓引数解析 + * */ + var qu = new Queue(args); // 引数をQueに登録 (qu.Enqueue(a)でも可能) while (qu.Count > 0) { // 引数のパース var wd = qu.Dequeue(); // 取り出しqu.Dequeue()で次の要素を取得する @@ -227,8 +196,8 @@ namespace CSRenderMain { ["/BB"] = "Bleed", ["/BC"] = "Crop", ["/BA"] = "Art" }; - if (isOpt("/?", "/H")) { - DispHelp(); + if (isOpt("/?", "/H","/Help")) { + DispHelpDetail(); return -1; } else if (isOpt("/F")) { pm.pdfPath = (qu.Count > 0) ? qu.Dequeue() : "";// next word. @@ -238,17 +207,24 @@ namespace CSRenderMain { pm.dpi = (qu.Count > 0) ? qu.Dequeue() : "";// next word. if (!double.TryParse(pm.dpi, out double dmy)) { Console.WriteLine($"解像度が不正です:/D {pm.dpi}"); - DispHelp(); + DispHelpNoArg(); return -1; } } else if (isOpt("/Para")) { string paraNum = (qu.Count > 0) ? qu.Dequeue() : "";// next word. if (!int.TryParse(paraNum, out pm.para)) { Console.WriteLine($"並行数が不正です:/para {paraNum}"); - DispHelp(); + DispHelpNoArg(); return -1; } - } else if (isOpt("/P")) { + } else if (isOpt("/ParaPage")) { + string paraNum = (qu.Count > 0) ? qu.Dequeue() : "";// next word. + if (!int.TryParse(paraNum, out pm.paraPage)){ + Console.WriteLine($"並行数(ページスレッド数が不正です:/paraPage {paraNum}"); + DispHelpNoArg(); + return -1; + } + } else if (isOpt("/P")) { pm.pageRange = (qu.Count > 0) ? qu.Dequeue() : "";// next word. } else if (isOpt("/JPG", "/JPEG")) { pm.imageType = "JPG"; @@ -264,7 +240,7 @@ namespace CSRenderMain { pm.jpegQ = (qu.Count > 0) ? qu.Dequeue() : "";// next word. if (!int.TryParse(pm.jpegQ, out int dmy)) { Console.WriteLine($"JPEG Qualityが不正です:/JPEGQ {pm.jpegQ}"); - DispHelp(); + DispHelpNoArg(); return -1; } if (!(0 < dmy && dmy <= 100)) { @@ -323,13 +299,14 @@ namespace CSRenderMain { pm.pdfPath = wd; continue; } - if (pm.bFC && (pm.pdfPath2 == "")) { - pm.pdfPath2 = wd;// 比較時のみ取得 + if (pm.bFC && (pm.pdfPathRef == "")) { + pm.pdfPathRef = wd;// 比較時のみ取得 continue; } } } // ↑引数解析終わり + setEcho(pm.bVerbose); if (pm.bVerbose) echo("Varbose Mode!"); @@ -339,6 +316,7 @@ namespace CSRenderMain { if (!bSubExe) { /// サーバー側 echo("++MainProcess start"); + Console.WriteLine($"LogicalProcNum={logicalProcessNum},ProcessParallelTasks={pm.para},PageParallelTasks={pm.paraPage}"); } else { // SubExe側の動作に差し替える var sp = pm.subExe.Split(':'); @@ -352,7 +330,7 @@ namespace CSRenderMain { if (pm.pdfPath == "") { Console.WriteLine("pdfファイルが指定されてません"); - DispHelp(); + DispHelpNoArg(); return -1; } if (!(File.Exists(pm.pdfPath) || Directory.Exists(pm.pdfPath))) { @@ -371,15 +349,15 @@ namespace CSRenderMain { // Directoryが指定されたのでファイルリストアップ bDirMode = true; pdfPathLst = System.IO.Directory.GetFiles(pm.pdfPath, "*.pdf"/*, System.IO.SearchOption.AllDirectories*/); - pdfPathLst = Array.ConvertAll(pdfPathLst, f => Path.GetFileName(f)); // 配列書き換え(ファイル名のみにする - // var enumLst = pdfPathLst.Select(f => Path.GetFileName(f)); LINQ式に置き換えることも可能(返り値は配列ではない) - //foreach ( var f in pdfPathLst) { - // Console.WriteLine($@"path1={f}"); - //} + pdfPathLst = Array.ConvertAll(pdfPathLst, f => Path.GetFileName(f)); + // 配列書き換え(ファイル名のみにする + // var enumLst = pdfPathLst.Select(f => Path.GetFileName(f)); LINQ式に置き換えることも可能(返り値は配列ではない) + //foreach ( var f in pdfPathLst) { + // Console.WriteLine($@"path1={f}"); + //} //Console.WriteLine($@"path1.Len={pdfPathLst.Count()}"); } - var resultData = new SortedDictionary(); if ((!pm.bFC) && bDir) { @@ -389,22 +367,22 @@ namespace CSRenderMain { } } if (pm.bFC) {// 比較モード時 - if (pm.pdfPath2 == "") { + if (pm.pdfPathRef == "") { Console.WriteLine("比較モードで2つ目のpdfファイルが指定されてません"); return -1; } - if (!(File.Exists(pm.pdfPath2) || Directory.Exists(pm.pdfPath2))) { - Console.WriteLine($"比較ファイルが存在しません:{pm.pdfPath2}"); + if (!(File.Exists(pm.pdfPathRef) || Directory.Exists(pm.pdfPathRef))) { + Console.WriteLine($"比較ファイルが存在しません:{pm.pdfPathRef}"); return -1; } - bool bDir2 = File.GetAttributes(pm.pdfPath2).HasFlag(FileAttributes.Directory); + bool bDir2 = File.GetAttributes(pm.pdfPathRef).HasFlag(FileAttributes.Directory); if (bDir2) { if (!bDir) { - Console.WriteLine($"比較対象はファイルパスでないといけません:{pm.pdfPath2}"); + Console.WriteLine($"比較対象はファイルパスでないといけません:{pm.pdfPathRef}"); return -1; } // 2つ目のファイルリストアップ - pdfPathLst2 = System.IO.Directory.GetFiles(pm.pdfPath2, "*.pdf"/*, System.IO.SearchOption.AllDirectories*/); + pdfPathLst2 = System.IO.Directory.GetFiles(pm.pdfPathRef, "*.pdf"/*, System.IO.SearchOption.AllDirectories*/); pdfPathLst2 = Array.ConvertAll(pdfPathLst2, f => Path.GetFileName(f));// 配列の書き換え // 共通のファイルを見つける。Lstの要素がLst2に含まれているかどうか @@ -412,13 +390,13 @@ namespace CSRenderMain { if (pdfPathLst2.Contains(f)) { pdfPathLstBoth.Add(f); } else { - pdfPathLstNoBoth.Add(f);// LstがLst2に含まれていない + pdfPathLstNoBoth.Add("tgt:"+f);// LstがLst2に含まれていない } } // Lst2のみファイルをNoBothに登録 foreach (var f in pdfPathLst2) { if (!pdfPathLstBoth.Contains(f)) { - pdfPathLstNoBoth.Add(f); + pdfPathLstNoBoth.Add("ref:"+f); } } // pdfPathBoth,pdfPathNoBothが作成済み @@ -433,12 +411,18 @@ namespace CSRenderMain { var watch = System.Diagnostics.Stopwatch.StartNew(); // 時間の生成と計測開始を同時に行う var otDir = pm.outuptImageDir; if (otDir == "") { - // 出力ディレクトリの作成 // 元PDFの同一フォルダにIMGフォルダを作成する - otDir = Path.Combine(Directory.GetParent(pm.pdfPath).FullName, "IMG"); - } - - if (!Directory.Exists(otDir) /* && (!bMkHash)*/ ) { // MkHashのとき不要→必要 - Directory.CreateDirectory(otDir); + if ( bDir ) { + var suffix = pm.bFC ? ".DiffImage": ".IMG"; + otDir = Path.Combine( Directory.GetParent(pm.pdfPath).FullName, Path.GetFileName( pm.pdfPath )+suffix); + echo($"****Output dir={otDir} ******\n"); + if (!Directory.Exists(otDir) && (!pm.bMkHash) ) { // + Directory.CreateDirectory(otDir); + } + } else { + // 出力ディレクトリの作成 // 元PDFの同一フォルダにIMGフォルダを作成する + otDir = Path.Combine(Directory.GetParent(pm.pdfPath).FullName, "IMG"); + } + pm.outuptImageDir = otDir;// pmに戻す } var otHashPath = ""; var otHashPath2 = ""; @@ -450,10 +434,10 @@ namespace CSRenderMain { //比較モード // ハッシュデータの読み込み echo($@"pre pdfPath={pm.pdfPath}"); - echo($@"pre pdfPath2={pm.pdfPath2}"); + echo($@"pre pdfPathRef={pm.pdfPathRef}"); var hashDataTgt = HashData.load(pm.pdfPath, hashBaseName, pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ, ref otHashPath); - var hashDataRef = HashData.load(pm.pdfPath2, hashBaseName, pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ, ref otHashPath2); + var hashDataRef = HashData.load(pm.pdfPathRef, hashBaseName, pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ, ref otHashPath2); //Console.WriteLine(""); if (pm.resultPath != "") {// 初期化 @@ -462,14 +446,15 @@ namespace CSRenderMain { ret = RenderPDF.RenderPdfDocCompare( pdfPath: Path.GetFullPath(pm.pdfPath), - refPdfPath: Path.GetFullPath(pm.pdfPath2), + refPdfPath: Path.GetFullPath(pm.pdfPathRef), resultDataPath: pm.resultPath, inDir : otDir, pm : rdCond, inPageRange : pm.pageRange, inHashDataTgt: hashDataTgt, inHashDataRef: hashDataRef, - bPDFium : pm.bPDFium + bPDFium : pm.bPDFium, + nPageThreadNum: pm.paraPage ); } else { // Sync version. @@ -488,8 +473,9 @@ namespace CSRenderMain { inPageRange : pm.pageRange, bSaveImage : pm.bMkHash ? false : true,// ハッシュ値生成ではイメージ保存しない。 bHash : pm.bHash, - inHashData : hashDataTgt, - bPDFium : pm.bPDFium + inHashData : pm.bMkHash ? hashDataTgt : null, //ハッシュコマンドモードのみDataを渡す + bPDFium : pm.bPDFium, + nPageThreadNum : pm.paraPage ); if (pm.bMkHash) { /// ここでMain側に通信データを返せばよい @@ -503,10 +489,10 @@ namespace CSRenderMain { //表示のみ まだやめておく: } } - // retはページ数です。 + // retはページ数を返す } } else { - // 対象がディレクトリ + // 対象がディレクトリの場合 echo("For Directory mode"); HashData hashDataTgt = null, hashDataRef = null; hashDataTgt = HashData.load(pm.pdfPath, hashBaseName, pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ, ref otHashPath); @@ -520,23 +506,16 @@ namespace CSRenderMain { } if (pm.bFC) { // リファレンス側のハッシュの読み込み - hashDataRef = HashData.load(pm.pdfPath2, hashBaseName, pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ, ref otHashPath2); + hashDataRef = HashData.load(pm.pdfPathRef, hashBaseName, pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ, ref otHashPath2); if (hashDataRef == null) { // 空のハッシュデータを作成 hashDataRef = new HashData(); hashDataRef.Head.SetRenderInfo(pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ); } } - // ハッシュデータが適切に作成済みがどうかを確認。もしハッシュデータが存在しない場合は - // ハッシュデータを渡してはいけない。ハッシュのFilesで確認するか? - // ハッシュデータの作成プロセスをもっと簡単にすべき echo($@"Start render:Count={pdfPathLstBoth.Count}"); //Console.WriteLine("");[ var count = pdfPathLstBoth.Count; - - //ParallelOptions options = new ParallelOptions { MaxDegreeOfParallelism = 6 }; - //Parallel.For(0,pdfPathLstBoth.Count, options, index => { - //逆に遅くなる UWPコール(render)は対応していない? if (pm.bExeSepa &&(!pm.bFC)) { // FCモードは除外します。 var tokenSource = new CancellationTokenSource(); ParallelOptions options = new ParallelOptions { MaxDegreeOfParallelism = pm.para }; @@ -544,12 +523,12 @@ namespace CSRenderMain { var loopResult = Parallel.For(0,pdfPathLstBoth.Count, options, (index,lpState) => { //for (var index = 0; index < pdfPathLstBoth.Count; index++) { var tgt = Path.Combine(pm.pdfPath, pdfPathLstBoth[index]); - var tgtID = $@"{index}/{pdfPathLstBoth.Count}"; + var tgtID = $@"{index+1}/{pdfPathLstBoth.Count}"; Console.WriteLine($@"**Remain({count})****tgt({tgtID})={tgt}"); if (pm.bFC) { //比較モード - var reff = Path.Combine(pm.pdfPath2, pdfPathLstBoth[index]); + var reff = Path.Combine(pm.pdfPathRef, pdfPathLstBoth[index]); } else { // Sync version(Paralles). var svrData = new xChangeWCFPipe.XData(); var pmClone = pm.Clone(); @@ -583,7 +562,7 @@ namespace CSRenderMain { if ( retProc < 1 ) { //PDFページ数以外が返った時 ret = -1; - Console.WriteLine( $@"Warning ::Cannot get PDFPage:{retProc}"); + Console.WriteLine( $@"Warning ::Cannot get PDFPage:{retProc},tgt={tgt},{argsStr}"); } else { ;// 初期値 ret=0; } @@ -613,13 +592,13 @@ namespace CSRenderMain { proc.Dispose(); svrHost.Close(); svrData = null; - Console.WriteLine($@"End tgt({tgtID})"); + echo($@"End tgt({tgtID})"); } //GC.Collect();// これでメモリリークが解決した Paraの中ではやめておく count--; }); if ( !loopResult.IsCompleted ) { - Console.WriteLine("中断"); + Console.WriteLine("Abort!"); pm.bMkHash = false;// Hash値保存抑制 } ////tokenSource.Cancel();// 処理のキャンセル @@ -627,8 +606,90 @@ namespace CSRenderMain { // pm.bMkHash = false;// Hash値保存抑制 //} //}; // Non Para Block - } else { // NoExeSepa + echo($"********** コマンド終了*****{argsStr} "); + } else if (pm.bExeSepa &&(pm.bFC)) { // セパのFCモード + /* + refのハッシュが存在しなければWCFで作成 + tgtのハッシュは強制的にWCFで作成 + */ + echo($"****************セパのFCモード******{argsStr}************"); + hashDataRef = HashData.load(pm.pdfPathRef, hashBaseName, pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ, ref otHashPath2); + if ( hashDataRef == null ) { + var myExePath = Assembly.GetExecutingAssembly().Location; + var proc = Ut.Ut.DoCmd( + cmdAndArgs: new string[] { myExePath, "/MkHash",pm.pdfPathRef,"/D",pm.dpi,$"/{pm.imageType }",$"/JPEGQ",pm.jpegQ } + , bEcho: true + , bSameConsole: true + ); + proc.WaitForExit(); + hashDataRef = HashData.load(pm.pdfPathRef, hashBaseName, pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ, ref otHashPath2); + if ( hashDataRef != null ) { + echo($" OK hash ref"); + } + } else { + echo($" OK hash ref(already exist)"); + } + if (true){ + var myExePath = Assembly.GetExecutingAssembly().Location; + var proc = Ut.Ut.DoCmd( + cmdAndArgs: new string[] { myExePath, "/MkHash",pm.pdfPath,"/D",pm.dpi, $"/{pm.imageType }",$"/JPEGQ",pm.jpegQ } + , bEcho: true + , bSameConsole: true + ); + proc.WaitForExit(); + hashDataTgt = HashData.load(pm.pdfPath, hashBaseName, pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ, ref otHashPath); + if ( hashDataTgt != null ) { + echo($" OK hash tgt"); + } + } + echo($@"****************FC開始*****{argsStr}**************"); + if (pm.resultPath != "") {// 初期化 + File.WriteAllText(Path.GetFullPath(pm.resultPath), ""); + } + int noMatchPageNum=0; + int noMatchFileNum=0; + for (var index = 0; index < pdfPathLstBoth.Count; index++) { + var tgt = Path.Combine(pm.pdfPath, pdfPathLstBoth[index]); + Console.WriteLine($@"tgt={index+1}/{pdfPathLstBoth.Count}:{tgt}"); + var reff = Path.Combine(pm.pdfPathRef, pdfPathLstBoth[index]); + ret = RenderPDF.RenderPdfDocCompare( + pdfPath: Path.GetFullPath(tgt), + refPdfPath: Path.GetFullPath(reff), + resultDataPath: pm.resultPath, + inDir: otDir, + pm: rdCond, + inPageRange: pm.pageRange, + inHashDataTgt: hashDataTgt, + inHashDataRef: hashDataRef, + bPDFium: pm.bPDFium, + nPageThreadNum:pm.paraPage + ); + if ( ret > 0 ) { + // retは不一致ページ数 + noMatchFileNum++; + noMatchPageNum+= ret; + } + }; + watch.Stop(); + + var SummaryResult = $"\n[結果]\n"; + SummaryResult += $"不整合ファイル群={pdfPathLstNoBoth.Count()}\n"; + foreach (var f in pdfPathLstNoBoth) { + SummaryResult += $"\tWarning [no match]={f}\n"; + } + SummaryResult += $"NGファイル数={noMatchFileNum}\n"; + SummaryResult += $"NGページ数 ={noMatchPageNum}\n"; + SummaryResult += $"Result={ret},time={ watch.ElapsedMilliseconds / 1000.0 }[sec]"; + Console.WriteLine(SummaryResult);// 結果標準出力 + if (pm.resultPath != "") {// ファイル出力の先頭に追記 + var rData = File.ReadAllText(pm.resultPath); + SummaryResult += "\n[詳細] *find @Difference\n"; + File.WriteAllText(Path.GetFullPath(pm.resultPath), (SummaryResult + rData)); + } + echo($"****************[終了]セパのFCモード***{argsStr}***************"); + } else { // NoExeSepa + echo($@"****************NoExeSepa*****{argsStr}**************"); if (pm.resultPath != "") {// 初期化 File.WriteAllText(Path.GetFullPath(pm.resultPath), ""); } @@ -637,7 +698,7 @@ namespace CSRenderMain { Console.WriteLine($@"tgt={index+1}/{pdfPathLstBoth.Count}:{tgt}"); if (pm.bFC) { //比較モード - var reff = Path.Combine(pm.pdfPath2, pdfPathLstBoth[index]); + var reff = Path.Combine(pm.pdfPathRef, pdfPathLstBoth[index]); //Console.WriteLine(""); ret = RenderPDF.RenderPdfDocCompare( pdfPath: Path.GetFullPath(tgt), @@ -648,8 +709,10 @@ namespace CSRenderMain { inPageRange: pm.pageRange, inHashDataTgt: hashDataTgt, inHashDataRef: hashDataRef, - bPDFium: pm.bPDFium + bPDFium: pm.bPDFium, + nPageThreadNum:pm.paraPage ); + } else { // Sync version(Paralles). ret = RenderPDF.RenderPdfDoc( pdfPath: Path.GetFullPath(tgt), @@ -659,7 +722,8 @@ namespace CSRenderMain { bSaveImage: pm.bMkHash ? false : true,// ハッシュ値生成ではイメージ保存しない。 bHash: pm.bHash,// bHash,dumy inHashData: pm.bMkHash ? hashDataTgt : null, //ハッシュコマンドモードのみDataを渡す - bPDFium: pm.bPDFium + bPDFium: pm.bPDFium, + nPageThreadNum: pm.paraPage ); } count--; @@ -672,16 +736,6 @@ namespace CSRenderMain { } watch.Stop(); Console.WriteLine($"result={ret},time={ watch.ElapsedMilliseconds / 1000.0 }[sec]"); -#if COMMNET - Assembly assm = Assembly.GetExecutingAssembly(); - Console.WriteLine(assm.FullName); - // 参照しているすべてのアセンブリを取得し、表示する - foreach (AssemblyName refassm in assm.GetReferencedAssemblies()) { - Console.WriteLine($@" {refassm.FullName},Ver({refassm.Version}),VerComp({refassm.VersionCompatibility})" ); - } - string clrVersion = System.Environment.Version.ToString(); - Console.WriteLine($@"clrVer={clrVersion}"); -#endif return ret;//success. } @@ -861,22 +915,6 @@ namespace xChangeWCFPipeGEN _service = oInstance; // デリゲート登録 //callDelegate = () => { Console.WriteLine("svr:Delegate func called"); } -#if false - { - // インターフェースを求める - // AddServiceEndpoint()にわたすIXDataはXData.Interfaces()から求められるが - // インタフェースは一つとは限らないのでやめておく。 - Type interfType = null; - Type type = _service.GetType(); - Type[] interfaces = type.GetInterfaces(); - Console.WriteLine($@"GetInterfaces***********************************"); - foreach (Type t in interfaces) { - Console.WriteLine(t.ToString()); - interfType = t; - }; - Console.WriteLine($@"End of GetInterfaces***********************************"); - } -#endif _serviceHost = new ServiceHost(_service, new Uri(uri)); try { _serviceHost.AddServiceEndpoint(_baseIf/*typeof(Ti)*/, new NetNamedPipeBinding(), pipeAddress); @@ -902,14 +940,9 @@ namespace xChangeWCFPipeGEN static public Ti makeCLT(string pipeName = "PDFormstudioESorGS", string pipeAddress = "SubModuleAddress") { const string pipeBase = "net.pipe://localhost"; var address = pipeBase + "/" + pipeName + "/" + pipeAddress; - //try { var factory = (new ChannelFactory(new NetNamedPipeBinding(), new EndpointAddress(address))); Ti toMain = factory.CreateChannel(); return toMain; - //} catch (Exception e) { - // Console.WriteLine("makeCLT failed:" + e.ToString()); - // return null; - //} } } } @@ -997,6 +1030,7 @@ namespace Ut /// /// ProcessID static int GetCurrentProcessId() { return Process.GetCurrentProcess().Id; } + /// /// 外部プログラムを起動する /// @@ -1008,7 +1042,8 @@ namespace Ut /// public static Process DoCmd(IEnumerable cmdAndArgs, bool bSameConsole = true, bool bEcho = true, bool bWait = false, int nWaitLimitMSec = -1) { var cmdName = cmdAndArgs.First();//[0]; - var argStr = makeCmdLine(cmdAndArgs.Skip(1));// 先頭を除外してコピー new ArraySegment(cmdAndArgs.ToArray(), 1, cmdAndArgs.Count()-1) + var argStr = makeCmdLine(cmdAndArgs.Skip(1)); + // 先頭を除外してコピー new ArraySegment(cmdAndArgs.ToArray(), 1, cmdAndArgs.Count()-1) //Console.WriteLine($@"DoCmd:{cmdName}:args[{argStr}]"); //using (var p = new Process()) { var p = new Process(); diff --git a/CSRender/Properties/AssemblyInfo.cs b/CSRender/Properties/AssemblyInfo.cs index be2612b..fe94bcf 100644 --- a/CSRender/Properties/AssemblyInfo.cs +++ b/CSRender/Properties/AssemblyInfo.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System.Resources; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -10,7 +11,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("CSRender")] -[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyCopyright("Copyright © 2021")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -32,9 +33,11 @@ using System.Runtime.InteropServices; // すべての値を指定するか、次を使用してビルド番号とリビジョン番号を既定に設定できます // 既定値にすることができます: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.3.0.5")] -[assembly: AssemblyFileVersion("1.3.0.5")] +[assembly: AssemblyVersion("1.3.1.0")] +[assembly: AssemblyFileVersion("1.3.1.0")] +[assembly: NeutralResourcesLanguage("ja")] +// rev:1.3.1.x:2021/10/05 :不要コード削除(Windows.Data.pdf) // rev:1.3.0.5:2021/09/14 :/Paraで並行プロセス最大値の設定追加 // rev:1.3.0.4:2021/04/29 :Helpの記述抜け "/Result"の対応 // rev:1.3.0.3:2020/11/22 :/Result <比較結果> を実装。diff画像を出力 diff --git a/CSRender/Program_Scrap.cs b/CSRender/Scrap1/Program_Scrap.cs similarity index 100% rename from CSRender/Program_Scrap.cs rename to CSRender/Scrap1/Program_Scrap.cs diff --git a/CSRender/Program_Scrap2.cs b/CSRender/Scrap1/Program_Scrap2.cs similarity index 100% rename from CSRender/Program_Scrap2.cs rename to CSRender/Scrap1/Program_Scrap2.cs diff --git a/CSRender/Program_Scrap3.cs b/CSRender/Scrap1/Program_Scrap3.cs similarity index 100% rename from CSRender/Program_Scrap3.cs rename to CSRender/Scrap1/Program_Scrap3.cs diff --git a/CSRender/Program_Scrap4_leakOK.cs b/CSRender/Scrap1/Program_Scrap4_leakOK.cs similarity index 100% rename from CSRender/Program_Scrap4_leakOK.cs rename to CSRender/Scrap1/Program_Scrap4_leakOK.cs diff --git a/CSRender/Program_mg0607.cs b/CSRender/Scrap1/Program_mg0607.cs similarity index 100% rename from CSRender/Program_mg0607.cs rename to CSRender/Scrap1/Program_mg0607.cs diff --git a/CSRender/Scrap2/CSRender_full.cs b/CSRender/Scrap2/CSRender_full.cs new file mode 100644 index 0000000..02d33d2 --- /dev/null +++ b/CSRender/Scrap2/CSRender_full.cs @@ -0,0 +1,1132 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media.Imaging; +using System.Reflection; // Assembly. +using System.Text.RegularExpressions; + +using System.Security.Cryptography; + +using System.Runtime.Serialization; +using System.Runtime.Serialization.Json; +using System.Collections.ObjectModel; + +using System.Runtime.InteropServices; + + +// for Stream conv. +using System.Runtime.InteropServices.WindowsRuntime; + +using Windows.Storage.Streams; + +//using System.IO.WindowsRuntimeStreamExtensions; + + +// + +//using Codeplex.Data; // DynamicJson + + +// UWP-APIの使用 +using Windows.Data.Pdf; + +// PDFiumの追加 +using PdfiumViewer; + +using static CSRender.RenderPDF; + + +//https://proself2.screen.co.jp/public/OcyIQAHPksNAnE4Bs3BxIPQApnAEmSCIBOxsCZ946Uur +//Screen8080 +//1_A4縦_2×1_両面_可変長レコード.pdf +// TOshiba: +//#region アセンブリ Windows, Version=255.255.255.255, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime +// C:\UserData\GIT_JSH\WinRT\CSRender\CSRender\bin\Debug\Windows.winmd +// WindowsRuntime 1.3 + +//#endregion +// MacBookPro: +// WindowsRuntime 1.4 +//CSRender, Version=1.1.0.0, Culture=neutral, PublicKeyToken=null +// mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089,Ver(4.0.0.0),VerComp(SameMachine) +// System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089,Ver(4.0.0.0),VerComp(SameMachine) +// Windows, Version=255.255.255.255, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime,Ver(255.255.255.255),VerComp(SameMachine) +// System.Runtime.WindowsRuntime, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089,Ver(4.0.0.0),VerComp(SameMachine) +// System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a,Ver(4.0.0.0),VerComp(SameMachine) +// System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089,Ver(4.0.0.0),VerComp(SameMachine) +// System.Runtime.Serialization, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089,Ver(4.0.0.0),VerComp(SameMachine) +// clrVer=4.0.30319.42000 +// 参考: zoomパレメータなど +// https://www.syncfusion.com/kb/8767/how-to-print-pdf-documents-in-xamarin-forms-platform +//var DisplayInformation = Windows.Graphics.Display.DisplayInformation.GetForCurrentView(); +//var dpi = DisplayInformation.LogicalDpi / 96; + +// https://elesynd.blogspot.com/2018/11/hDpiForm.html +//Program.csでDPIAwareする +//FormクラスのAutoScaleModeをUIを使って"Dpi"に変更する +//それでもズレるコントロールは、FontをUIを使って明示的に指定してみる +// https://qiita.com/felis_silv/items/efee4b1a397b0b95100a +// スケーリング grph.FromImage(bmp)後にスケール + +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; + + const int LOGPIXELSX = 88; + const int LOGPIXELSY = 90; + // + // + const int HORZSIZE = 4;//物理画面の幅・高さ(ミリメートル単位) + const int VERTSIZE = 5; + const int HORZRES = 8; //画面の幅・高さ(ピクセル単位) + const int VERZRES = 10; + // + const int RASTERCAPS = 38; + // 返却値のマスク + //1 (RC_BITBLT) ビットマップの転送 + //2 (RC_BANDING) バンド処理のサポートが必要 + //4 (RC_SCALING) スケーリング + //8 (RC_BITMAP64) 64KB より大きいビットマップ + //0x0080 (RC_DI_BITMAP) SetDIBits 関数と GetDIBits 関数 + //0x0100 (RC_PALETTE) デバイスはパレットベースのデバイスである + //0x0200 (RC_DIBTODEV) SetDIBitsToDevice 関数 + //0x0800 (RC_STRETCHBLT) StretchBlt 関数 + //0x1000 (RC_FLOODFILL) 塗りつぶし + //0x2000 (RC_STRETCHDIB) StretchDIBits 関数 + const int SCALINGFACTORX = 114;//x 軸・ y 軸のスケーリングファクター + const int SCALINGFACTORY = 115; + + [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, LOGPIXELSX); + if ( bDump ) { + Console.WriteLine("DpiX: {0}", GetDeviceCaps(dc, LOGPIXELSX)); + Console.WriteLine("DpiY: {0}", GetDeviceCaps(dc, LOGPIXELSY)); + + Console.WriteLine("SCALINGFACTORYX: {0}", GetDeviceCaps(dc, SCALINGFACTORX)); + Console.WriteLine("SCALINGFACTORY: {0}", GetDeviceCaps(dc, SCALINGFACTORY)); + //if ( false ) {// これは倍率で変更されない + // int width = System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width; // 幅(pixel) + // int height = System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height; // 高さ(pixel) + // // 取得したスクリーンのサイズをコンソールに出力する + // string message = string.Format("This screen size is {0} x {1} (pixel)", width, height); + // System.Console.WriteLine(message); + //} + + //if (false){// コンソールアプリでは呼び出せない + // var di = Windows.Graphics.Display.DisplayInformation.GetForCurrentView(); + // Console.WriteLine($@"di.LogicalDpi: {di.LogicalDpi}"); + // Console.WriteLine($@"di.RawDpiX: {di.RawDpiX}"); + // Console.WriteLine($@"di.ResoSacle: {di.ResolutionScale.ToString()}"); + // Console.WriteLine($@"di.StereoEnabled: {di.StereoEnabled}"); + // Console.WriteLine($@"di: {di.ToString()}"); + //} + } + ReleaseDC(IntPtr.Zero, dc); + return dpi;// 96;// dpi; + } + } + public static class RenderPDF + { + /// + /// Rendering Condition + /// + [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 + } + + // Verbose + public static void setEcho(bool b) { + bEcho = b; + } + public static bool bEcho = false; + public static void echo(params object[] args) { + if (!bEcho) return; + var s = ""; + foreach (object a in args) { + s += a.ToString(); + } + Console.WriteLine(s); + } + + // 事前チェック BashHash + public static bool RenderPdfInitCheck( Stream stream /* PDFストリーム */) { + Windows.Storage.Streams.InMemoryRandomAccessStream rmStrageStream=null; + + {// Stream->Memstream -> byte array -> IBuffer -> InMemmory...Stream() + var ms = new MemoryStream(); + stream.CopyTo(ms); + var bAray = ms.ToArray(); + IBuffer ib = bAray.AsBuffer(); + rmStrageStream = new Windows.Storage.Streams.InMemoryRandomAccessStream(); + //await s.WriteAsync(ib); + async Task wa() => await rmStrageStream.WriteAsync(ib); + var wa_ret = wa().Result; + } + async Task LoadS() => await Windows.Data.Pdf.PdfDocument.LoadFromStreamAsync(rmStrageStream); + var pdfDoc = LoadS().Result; + if (pdfDoc == null) { + Console.WriteLine("error: get PdfDocument failed"); + return false; + } + + // var memStream = new MemoryStream(); + // memStream.AsInputStream(); + // WindowsRuntimeStreamExtensions. + // var ras = WindowsRuntimeStreamExtensions.AsRandomAccessStream(stream); + //https://blog.ch3cooh.jp/entry/20131207/1386342000 + //https://csharp.hotexamples.com/site/file?hash=0xe951daae5be57f9f35507c45eb09a1085819b0a42b846aaa211ac138a2d1b5af&fullName=src/Nutrition/Nutrition.WP/WPOCRService.cs&project=SamirHafez/Nutrition + + //なんだけどな。。。→ .NetFrameworkにAsRandomAccessStream(x)が存在しない。.Net Coreのみみたい。 + + // exeと同一場所に"baseRender.jpg" + string otPath = Path.Combine(Directory.GetParent(Assembly.GetEntryAssembly().Location).FullName,"baseRender.jpg"); + bool bSaveImage = false;// Debug時にtrueにする + Console.WriteLine($"mode=Hash base check"); + var taskList = new List>(); + var hashValue = ""; + + var pm = new RenderConditionParams(); + var ret = RenderPage( + doc: pdfDoc, + page: pdfDoc.GetPage(0), + otPath: otPath, + pm:pm, + bSaveImage: bSaveImage, + bHash: true, + otHashCode: ref hashValue + ); + Console.WriteLine($@"Hash={hashValue},Host={Environment.MachineName},{Environment.OSVersion}"); + return true;// true:OK, false:NG + } + + // Sync version( no async -> Pallale Task ) ************************************************************************************** + + /// + /// PDFのレンダリング + /// + /// + /// + /// レンダリングの条件 + /// + /// + /// + /// 未使用,常にtrueで使用 + /// + /// + 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 + ) { + if (pm == null) { + pm = new RenderConditionParams(); + } + var otDir = inDir; + var otBaseName = Path.GetFileName(pdfPath);//*.pdf + var otExtention = pm.ImageType.ToLower(); + if (bSaveImage && otDir == "") { + // 出力ディレクトリが空→元PDFの同一フォルダにIMGフォルダを作成する + otDir = Path.Combine(Directory.GetParent(pdfPath).FullName, "IMG"); + if (!Directory.Exists(otDir)) + Directory.CreateDirectory(otDir); + } + // PDFファイルを読み込む + async Task GetStrageFilePath(string path) + => await Windows.Storage.StorageFile.GetFileFromPathAsync(path); + var file = GetStrageFilePath(pdfPath).Result; + + + async Task Load(Windows.Storage.StorageFile path) + => await Windows.Data.Pdf.PdfDocument.LoadFromFileAsync(file); + var pdfDoc = Load(file).Result; + + // 2つを合わせることができそう + //async Task GetLoadFromPath(string path) + //{ + // var a1 = await Windows.Storage.StorageFile.GetFileFromPathAsync(path); + // var a2 = await Windows.Data.Pdf.PdfDocument.LoadFromFileAsync(a1); + // return a2; + //}; + //var pdfDoc2 = GetLoadFromPath(pdfPath).Result; + + + if (pdfDoc == null) { + Console.WriteLine("error: get PdfDocument failed"); + return -1; + } + var n = pdfDoc.PageCount; + inMode = "pageBitMap"; + //Console.WriteLine($"mode={inMode},bPDFium={bPDFium}"); + var taskList = new List>(); + uint[] rNo = makePageRange(inPageRange, pdfDoc.PageCount); + + if (inHashData != null) { + // ハッシュ対象の最大ページ数の設定必須。 + inHashData.SetFile(pdfPath, (int)pdfDoc.PageCount); + } + // PDFiumViewer + PdfiumViewer.PdfDocument docG = null; + if (bPDFium) { + docG = PdfiumViewer.PdfDocument.Load(pdfPath); + } + //Console.WriteLine($@"dpi={pm.Dpi}"); + + /////並行処理するスレッド数を指定(2-4ぐらいが穏便な数値) + ParallelOptions options = new ParallelOptions { MaxDegreeOfParallelism = 4 }; + Parallel.ForEach(rNo, options, i => { + //Console.WriteLine($"*****{i}*****/{rNo.Length}"); + var hashValue = ""; + var ret = RenderPage( + doc: pdfDoc, + page: pdfDoc.GetPage((uint)(i - 1)), + otPath: Path.Combine(otDir, $"{otBaseName}.{i}.{otExtention}"), + pm: pm, + bSaveImage: bSaveImage, // ファイル保存 + bHash: bHash, + otHashCode: ref hashValue, + docG : docG + ); + if (inHashData != null) { + lock (inHashData) { + inHashData.AddHashCode(pdfPath, (int)i, hashValue); + } + } + }); + if ( docG != null) + docG.Dispose(); + + return (int)pdfDoc.PageCount;// ページ数を返却します + //return 0;// success. + } + /// + /// pdfのページを画像に出力する + /// + /// PdfDocument + /// ページ番号 + /// 出力ファイル名 + /// 正常:0 + public static int RenderPage( + // Need: + Windows.Data.Pdf.PdfDocument doc, // Need GetPage() only. (Total page number); + PdfPage page, // Target Page Data(page.index:0-x) + string otPath, + ref string otHashCode, + // Optional: + RenderConditionParams pm = null, + bool bSaveImage = true,// ファイル保存有無。ハッシュ値計算のみのときにfalseにする + bool bHash = false, + PdfiumViewer.PdfDocument docG = null // + ){ + if( pm == null) { + pm = new RenderConditionParams(); + } + double dpiWin = Check.GetDPI(); //96.0;/* , dpiPDF = 72.0*/ + //double resoScale = (96.0 / dpiWin); + double resoScale = (pm.Dpi / 96.0); + //double resoScale = (inDpi / dpiWin); + + var bDump = Check.bDump; + var bThDump = Check.bThDump; + + var opt = new PdfPageRenderOptions() { + // SEE:https://github.com/microsoft/Windows-universal-samples/blob/master/Samples/PdfDocument/cs/Scenario1_Render.xaml.cs + //BitmapEncoderId = Windows.Graphics.Imaging.BitmapEncoder.JpegEncoderId,// JPEG + IsIgnoringHighContrast = false + }; + var boxDic = new Dictionary() { + // Init + ["Media"] = page.Dimensions.MediaBox, + ["Trim"] = page.Dimensions.TrimBox, + ["Bleed"] = page.Dimensions.BleedBox, + ["Crop"] = page.Dimensions.CropBox, + ["Art"] = page.Dimensions.ArtBox + }; + //Dump: + if ( bDump){ + Console.WriteLine($@"PageZoom={page.PreferredZoom}"); + Console.WriteLine($@"PageRotate={page.Rotation.ToString()}"); + + Console.WriteLine($"{"Size",-5}=({page.Size.Width,8:f2},{page.Size.Height,8:f2})"); + foreach (var b in boxDic.Keys) { + var r = boxDic[b]; + Console.WriteLine($"{b,-5}=({r.X,8:f2},{r.Y,8:f2}) W({r.Width,8:f2},{r.Height,8:f2})"); + Console.WriteLine($"{b,-5}=L {r.Left,-8:f2} R {r.Right,-8:f2}) T {r.Top,-8:f2} B {r.Bottom,-8:f2})"); + } + } + + opt.SourceRect = boxDic.ContainsKey(pm.BoxType) ? boxDic[pm.BoxType] : boxDic["Crop"];// Cannot find -> "Crop"; + //Console.WriteLine($"Select {inBoxType} box."); + + // 何も変倍しないとどうなる + opt.DestinationWidth = (uint)(opt.SourceRect.Width * resoScale + 0.5); + opt.DestinationHeight = (uint)(opt.SourceRect.Height * resoScale + 0.5); + + // 150%の時は * 0.5358を設定するとよい解像度になるが、この数値の計算方法がわからない + //opt.DestinationWidth = (uint)(opt.SourceRect.Width * 0.5358/*resoScale*/ + 0.5); + //opt.DestinationHeight = (uint)(opt.SourceRect.Height * 0.5358/*resoScale*/ + 0.5); + + if (bDump) { + Console.WriteLine($"dpi={pm.Dpi},scale={resoScale},width(org)={page.Size.Width}->{opt.DestinationWidth}"); + Console.WriteLine($"dpi={pm.Dpi},scale={resoScale},height(org)={page.Size.Height}->{opt.DestinationHeight}"); + } + using (var memStrm = new Windows.Storage.Streams.InMemoryRandomAccessStream()) { + + if (docG == null) { + async Task RenderToStream(PdfPage p) => await p.RenderToStreamAsync(memStrm, opt); + RenderToStream(page).Wait(); + } else { + PdfRenderFlags flg = (PdfRenderFlags.ForPrinting | PdfRenderFlags.CorrectFromDpi); + System.Drawing.Image img = docG.Render((int)(page.Index), (float)pm.Dpi, (float)pm.Dpi, flg); + img.Save(memStrm.AsStream(), System.Drawing.Imaging.ImageFormat.Jpeg); + // [注意] PDFuim用に直接 AsStream()に書き込んでいるので、AsStream().Flush()しないとだめです + } + + async Task FlushX() => await memStrm.FlushAsync(); + FlushX().Wait(); + + var bmp = new System.Drawing.Bitmap(memStrm.AsStream()); + if (bDump) { + Console.WriteLine($"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 ) { + Console.WriteLine($"ot[th{th},{(page.Index + 1)}/{doc.PageCount}]=,{Path.GetFileName(otPath)},hash:{otHashCode}"); + } + return 0; + } + + var imEnc = new UtImage.Enc(pm.ImageType) { JpegQuality = pm.JpegQ };// Init membrers. + bmp.SetResolution((float)pm.Dpi, (float)pm.Dpi); + if (bThDump) { + Console.WriteLine($"ot[th{th},{(page.Index + 1)}/{doc.PageCount}]={Path.GetFileName(otPath)}({Directory.GetParent(otPath)})"); + } + // + imEnc.SaveImage(bmp, otPath); + // + bmp.Dispose(); + //memStrm.Seek(0); + } + return 0;//success + } + + // Sync version( 比較モード(/FC ) ************************************************************************************** + + /// + /// レンダリング(比較モード) + /// + /// + /// + /// + /// レンダリング条件 + /// + /// + /// 未使用常にtrueで利用する + /// + /// + /// 一致した場合は0、不一致の場合は!0を返却する + 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 + // 比較結果を返す ページ番号とメッセージ + ) { + + + var bDump = Check.bDump; + var bThDump = Check.bThDump; + + if(bDump) + Console.WriteLine("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 == "") { + // 出力ディレクトリが空→元PDFの同一フォルダにIMGフォルダを作成する + otDir = Path.Combine(Directory.GetParent(pdfPath).FullName, "IMG"); + if (!Directory.Exists(otDir)) + Directory.CreateDirectory(otDir); + } + // PDFファイルを読み込む + + + + // 以下のコードで良いみたい。 + // [変更待ち] var file2 = Windows.Storage.StorageFile.GetFileFromPathAsync(pdfPath).GetAwaiter().GetResult(); + + async Task GetStrageFilePath(string path) + => await Windows.Storage.StorageFile.GetFileFromPathAsync(path); + var file = GetStrageFilePath(pdfPath).Result; + async Task Load(Windows.Storage.StorageFile path) + => await Windows.Data.Pdf.PdfDocument.LoadFromFileAsync(path); + var pdfDoc = Load(file).Result; + if (pdfDoc == null) { + Console.WriteLine("error: get PdfDocument failed"); + return -1; + } + + // ref + var fileRef = GetStrageFilePath(refPdfPath).Result; + var pdfDocRef = Load(fileRef).Result; + if (pdfDocRef == null) { + Console.WriteLine("error: get PdfDocument(ref) failed"); + return -1; + } + + var n = pdfDoc.PageCount; + if ( n != pdfDocRef.PageCount) { + Console.WriteLine($"error: differ page length:{n}:{pdfDocRef.PageCount}"); + return -1; + } + uint[] rNo = makePageRange(inPageRange, n); + + int nRetAll = 0;// 一致(def) + + // ハッシュ情報の確認 + /* + - 双方のハッシュ値が存在するか確認 + - 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();// 比較結果を返す ページ番号とメッセージ + + // PDFiumViewer + PdfiumViewer.PdfDocument docG = null; + PdfiumViewer.PdfDocument docGRef = null; + if (bPDFium) { + docG = PdfiumViewer.PdfDocument.Load(pdfPath); + docGRef = PdfiumViewer.PdfDocument.Load(refPdfPath); + } + + + /////並行処理するスレッド数を指定(2-4ぐらいが穏便な数値) + ParallelOptions options = new ParallelOptions { MaxDegreeOfParallelism = 4 }; + Parallel.ForEach(rNo, options, i => { + Windows.Storage.Streams.InMemoryRandomAccessStream tgtStrm = null, refStrm = null; + string tgtHash = "", refHash = ""; + // ターゲット レンダー + InMemoryRandomAccessStream RendPageTGT() { + var strm = RenderPageStream(doc:pdfDoc,page:pdfDoc.GetPage((uint)(i-1)), pm:pm,docG:docG); + return strm; + } + // リファレンス レンダー + InMemoryRandomAccessStream RendPageREF() { + var strm = RenderPageStream(doc:pdfDocRef,page:pdfDocRef.GetPage((uint)(i-1)), pm:pm,docG:docGRef); + return strm; + } + //tgtStrm = RendPageTGT(); + + if (tgtHf == null) { + tgtStrm = RendPageTGT(); + tgtHash = GetHashValue(tgtStrm); + } else { + lock (inHashDataTgt) { + tgtHash = tgtHf.GetHashValue((int)(i - 1)); + } + } + // リファレンス + if (refHf == null) { + refStrm = RendPageREF(); + refHash = GetHashValue(refStrm); + } else { + lock (inHashDataRef) { + refHash = refHf.GetHashValue((int)(i - 1)); + } + } + // compare stream; + int nRet = 1;//異なる(def). + var dateStr = DateTime.Now.ToString(); + //Console.WriteLine($@"tgt={i}:{tgtHash}"); + //Console.WriteLine($@"ref={i}:{refHash}"); + + if (tgtHash != refHash) { + nRet = 1; + nRetAll = nRet;//全体の不一致設定 + echo($"{i}:NotMatch:{i},tgt[{tgtHash}],ref[{refHash}]"); + //2020年3月2日 15:23:55:[@Difference]:.diff.jpg + lock (fcResultMsg) { + fcResultMsg.Add((int)i, $@"{dateStr}:[@Difference]:{pdfPath}.diff.jpg"); + } + + //if ( true && tgtStrm != null && bSave) { + { + echo("***** save diff image"); + // diffのjpegを書き出してみる(tgt) + if (tgtStrm == null) tgtStrm = RendPageTGT(); + var otPath = Path.Combine(otDir, $"{otBaseName}.{i}.tgt.{otExtention}"); + var bmp = new System.Drawing.Bitmap(tgtStrm.AsStream()); + 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(); + } + //if (true && refStrm != null && bSave) { + { + // diffのjpegを書き出してみる(ref) + if (refStrm == null) refStrm = RendPageREF(); + var otPath = Path.Combine(otDir, $"{otBaseName}.{i}.ref.{otExtention}"); + var bmp = new System.Drawing.Bitmap(refStrm.AsStream()); + 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 { + nRet = 0;// match + //Console.WriteLine($"{i}:Match:{i}"); + // 2019年12月18日 19:31:38:[OK]:fname.pdf.1.png + lock (fcResultMsg) { + fcResultMsg.Add((int)i, $@"{dateStr}:[OK]:{Path.GetFileName(pdfPath)}.{i}.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. + } + + /// + /// 比較モード + /// + /// PdfDocument + /// ページ番号 + /// 出力ファイル名 + /// 正常:0 + public static Windows.Storage.Streams.InMemoryRandomAccessStream RenderPageStream( + // Need: + Windows.Data.Pdf.PdfDocument doc, // Need GetPage() only. (Total page number); + PdfPage page, // Target Page Data(page.index:0-x) + //string otPath, + // Optional: + RenderConditionParams pm = null, + PdfiumViewer.PdfDocument docG = null // + ) { + var bDump = Check.bDump; + var bThDump = Check.bThDump; + + if (pm == null) + pm = new RenderConditionParams(); + double dpiWin = Check.GetDPI(); //96.0;/* , dpiPDF = 72.0*/ + double resoScale = (pm.Dpi / dpiWin); + var opt = new PdfPageRenderOptions() { + // SEE:https://github.com/microsoft/Windows-universal-samples/blob/master/Samples/PdfDocument/cs/Scenario1_Render.xaml.cs + //BitmapEncoderId = Windows.Graphics.Imaging.BitmapEncoder.JpegEncoderId,// JPEG + IsIgnoringHighContrast = false + }; + // + //async Task PreparePage(PdfPage p) => await p.PreparePageAsync();//ページの完成を待つ + //PreparePage(page).Wait(); + //page.PreparePageAsync + + var boxDic = new Dictionary() { + // Init + ["Media"] = page.Dimensions.MediaBox, + ["Trim"] = page.Dimensions.TrimBox, + ["Bleed"] = page.Dimensions.BleedBox, + ["Crop"] = page.Dimensions.CropBox, + ["Art"] = page.Dimensions.ArtBox + }; + opt.SourceRect = boxDic.ContainsKey(pm.BoxType) ? boxDic[pm.BoxType] : boxDic["Crop"];// Cannot find -> "Crop"; + + opt.DestinationWidth = (uint)(opt.SourceRect.Width * resoScale + 0.5); + opt.DestinationHeight = (uint)(opt.SourceRect.Height * resoScale + 0.5); + if (bDump) + Console.WriteLine($"dpi={pm.Dpi},scale={resoScale},height(org)={page.Size.Height}->{opt.DestinationHeight}"); + var memStrm = new Windows.Storage.Streams.InMemoryRandomAccessStream(); + if(true) { + if (docG == null) { + async Task RenderToStream(PdfPage p) => await p.RenderToStreamAsync(memStrm, opt); + RenderToStream(page).Wait(); + } else { + PdfRenderFlags flg = (PdfRenderFlags.ForPrinting | PdfRenderFlags.CorrectFromDpi); + System.Drawing.Image img = docG.Render((int)(page.Index), (float)pm.Dpi, (float)pm.Dpi, flg); + img.Save(memStrm.AsStream(), System.Drawing.Imaging.ImageFormat.Jpeg); + // [注意] PDFuim用に直接 AsStream()に書き込んでいるので、AsStream().Flush()しないとだめです + memStrm.AsStream().Flush(); + } + async Task FlushX() => await memStrm.FlushAsync(); + //var dmy =FlushX().Result; + FlushX().Wait(); + } + //page.Dispose(); + + return memStrm;// + } + + /// + /// ページ1から始まるページ範囲の配列を返す + /// + /// + /// + /// + private static uint[] makePageRange(string pageRange = "1", uint endPage = 100) { + var range = new SortedSet(); + 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(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(); + } + public static string GetHashValue(Windows.Storage.Streams.InMemoryRandomAccessStream strm) { + var alg = new SHA256CryptoServiceProvider(); + strm.Seek(0); + var bin = alg.ComputeHash(strm.AsStream()); + alg.Clear(); + // バイト配列をUTF8エンコードで文字列化 + var hashedText = new StringBuilder(); + foreach (var b in bin) { + hashedText.AppendFormat("{0:X2}", b); + } + return hashedText.ToString(); + } + } + + /// + /// ファイルのハッシュ値のIO + /// + 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 PageHashCode = new List(); // ページ毎のハッシュ値の配列 + // メソッド + 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 Files = new SortedDictionary(); + + // メソッド + 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 + 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(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(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] : ""; + } + } + + } + + namespace UtImage // Image保存のユーティリティー + { + // ref:https://water2litter.net/rum/post/cs_pdf_wpf/ + //using EncType = System.Drawing.Imaging.Encoder;// 別名 + //using EncParamType = System.Drawing.Imaging.EncoderParameter;//別名 + + /// + /// 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"); + /// + /// + 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; + } + /// + /// サポートするEncodeパラメータ一覧 + /// 現状Errorが発生して取得できない + /// + /// 対象のBitMap + 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(); + //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; + } + } + + } +} diff --git a/CSRender/Scrap2/Program_full.cs b/CSRender/Scrap2/Program_full.cs new file mode 100644 index 0000000..74eb522 --- /dev/null +++ b/CSRender/Scrap2/Program_full.cs @@ -0,0 +1,1046 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; +//using System.Text; +//using System.Threading; +//using System.Threading.Tasks; +//using System.Windows; +//using System.Windows.Media.Imaging; +using System.Reflection; // Assembly. +using System.Text.RegularExpressions; + +using System.Threading; +using System.Threading.Tasks; + +using System.Runtime.InteropServices; + +using System.Runtime.Serialization; +using System.ServiceModel; // WCF +using System.Diagnostics; // for Process + + +// 作成クラス +using CSRender; +using CSRender.UtHash; +using static CSRender.RenderPDF; + +// *重要* デスクトップアプリで UWP Api を呼び出す プロジェクトの設定方法が記載されている +// https://docs.microsoft.com/ja-jp/windows/apps/desktop/modernize/desktop-to-uwp-enhance +// 参照はwindows. winmd :C:\Program Files (x86) \Windows Kits\10\UnionMetadata\/ファサード + + +namespace CSRenderMain { + [DataContract] + //[Serializable()] + public class ParamData { + [DataMember] + public string pdfPath = ""; //対象ファイル + [DataMember] + public string pdfPath2 = ""; //比較ファイル + [DataMember] + public string outuptImageDir = ""; // /O + [DataMember] + public string dpi = "72.0"; // /D + [DataMember] + public int para = 4; // 並行数 + [DataMember] + public string boxSelect = "Crop"; // /B + [DataMember] + public string pageRange = "1-*"; // /P + [DataMember] + public string mode = "page"; // /MODE + [DataMember] + public string imageType = "JPG"; // /JPG or /PNG + [DataMember] + public string jpegQ = "91"; // JPEGQ + [DataMember] + public bool bHash = false; // ハッシュ値を生成する + [DataMember] + public bool bMkHash = false; // ハッシュファイルを作成する + [DataMember] + public bool bFC = false; + [DataMember] + public string resultPath = ""; + [DataMember] + public bool bPDFium = true; + // + [DataMember] + public bool bExeSepa = true;// 実行分離 + [DataMember] + public string subExe = null;// "Sub"が指定されたら、処理をExe分離する。内部コマンド + // データはWCFの通信データを使う。引数はPipeName:PortAddressNameとする + [DataMember] + public bool bVerbose = false;// 詳細デバッグ + + public ParamData Clone() { + return (ParamData)MemberwiseClone(); + } + } + public class Program { + static void DispHelp() { + var pgName = Path.GetFileName(Path.GetFileName(Assembly.GetExecutingAssembly().Location));// プログラム名 + var pgNameFullPath = Assembly.GetExecutingAssembly().Location;// プログラム名 + Console.WriteLine("pageNameF="+pgNameFullPath); + + string msg = + $"{pgName} [/] \n" + + $"* Render of PDF file. available 3 command mode:[Basic Rendering] [Make Hash command] [Compare command], and [Render Options]\n" + + $"\tPDFの画像化は[Basic Rendering]。\n" + + $"\t比較は/MkHash([Make Hash command])後に、/FC([Compare command])で高速実行できます。\n" + + $"\n" + + $"[Basic Rendering] 基本的なレンダリング\n" + + $"\t{pgName} [/] \n" + + $"\t/F : pdfPath(pdfファイル名|ディレクトリ) /Fは省略可能\n" + + $"\t/O : 出力ディレクトリ。省略時は\"IMG\"フォルダ\n" + + $"\n" + + $"[Render Options] レンダリングオプション\n" + + $"\t/D <解像度> : 解像度指定 9 - 300dpi(default=72dpi)\n" + + $"\t/JPG,/JPEG,/PNG,/TIF,/TIFF,/GIF,/BMP: Select one output format.(default=/JPG)\n" + + $"\t/JPEGQ : Jpegの品質指定1-100(default=91)\n" + + $"\n" + + $"\t/P : ページの範囲を指定する(省略時は全ページ)\n" + + $"\t\t連続した範囲を指定する場合は、ハイフン('-')を用いる。終了側を省略すると最終pageまで。\n" + + $"\t\t複数のページを指定する場合は、カンマ(',')を用いる\n" + + $"\t\tEx. /P \"1,2,30-100\" //1,2pages and 30-100pages.\n" + + $"\t[Unsupport] 未対応↓\n" + + $"\t/L : 入力PDFファイルリスト(*unsupport)\n" + + $"\t/T : テンポラリフォルダを指定(省略時は出力先フォルダと同じ(*unsupport no need)\n" + + $"\t/OP <0|1> : オーバープリントのOn/Off (省略時は1)(*unsupport allways on[1])\n" + + $"\t/U <0|1> : 同名上書き設定 0:上書きしない 1:上書き(*unsupport allways overwrite[1])\n" + + $"\t/OFFSET : ミリ単位でオフセットを指定する(省略時は共に0mm)(*unsupport)\n" + + $"\t/PDFium <0or1>: GoogoleのPDFiumViewerエンジンを使用する(default=1>\n" + + $"\t【未】/BM,/BT,/BA,/BA,/BC: Select one box.(default=/BC:CrobBox): Boxies:MediaBox/BleedBox/TrimBox/ArtBox/CropBox\n" + + + $"\n" + + $"[Make Hash command] 比較用ハッシュ値作成コマンド\n" + + $"\t{pgName} /MkHash ...... \n" + + $"\t/MKHash : ハッシュ値を出力する。前記の[Render Options]を指定すること\n" + + $"\t[HASHファイル作成]\n" + + + $"\n" + + $"[Compare command] 比較コマンド\n" + + $"\t{pgName} /FC ...... \n" + + $"\t/FC : 2つのPDFを比較する。前記の[Render Options]を指定すること。無名引数が2つ必要です\n" + + $"\t 事前に/MkHashを実行しておくことで高速に処理できる\n" + + $"\n" + + $"\t/RESULT : 比較結果を格納するファイルパス\n" + + $"\t\t/FCコマンドを指定すると一致したら0,不一致なら1を返却するようになる\n" + + $"\t\tは、,<[OK] or [@Difference]>の行で構成される\n" + + + $"\n" + + $"[ELSE ] その他のオプション\n" + + $"\t/PDFium <0|1>:PDFiumライブラリを使う,デフォルト=1\n" + + $"\t/NoExeSepa :実行分離しない(遅い)\n" + + $"\t/para <並行数>:並行数を指定(デフォルト4)\n" + + $"\t内部コマンド:/SubExe :実行分離,PDF単位で別Processで処理\n" + + $"\n" + + $"/H or /? : This help\n" + + + /* PDFと同じフォルダに + data.<出力条件>.jsonファイルを作成する + 出力条件:+++ + data.72_BT_JPG_Q34.json + 既に存在したら、それを読んでから、追記(もしくは書き換える>。 + */ + + // Remove TEST + //+$"\t[for TEST]\n" + //+$"\t/M Sync(default) or ASync\n" + //+$"\t/M : pageBitMap(default), pageBitMapImage, page(same as pageBitMap), org(orginal source)\n" + + "\n"; + + Console.Write(msg); + } +#if COMENT +// 設計の見直し + +* ハッシュ作成のみのコマンド + → /MkHashをつけると ハッシュファイルのみ作成。ただし、ディレクトリ指定に限る + → CSRender /MkHash <各種Renderパラメータ> + +* 単純なRenderを維持。それに同時にハッシュファイルのOn/Off + → /MkHashをつけなければよい。 + → CSRender <各種Renderパラメータ> + +* 比較モードは、Autoハッシュで、存在しなければ最初に作成。Target/Refともに。比較しながら作成してよし + Manualでハッシュなしで、遅いけどできるようにする。ハッシュ動作の検証用に + → /FCをつけると、デフォルトでハッシュ優先で検査(Auto相当)、ハッシュ検査を無効にしたければ + → /NoHash追加でハッシュを無視してTarget/Refとも再計算。ハッシュを使いたければ、事前に/MkHashで + 作成しておけばよい。 +* ヘルプは <単純なRender> <ハッシュ作成> <比較>にわける + * <単純なレンダラ>: 1ファイルおよびディレクトリの比較。 Renderingパラメータの一覧(項目分ける)まで. + * <ハッシュ作成>: /MkHashで高速比較のための前準備。ディレクトリモードでハッシュファイルのみを作成する。Renderingパラメータは合わせること + * <比較>:/FCコマンドで比較の説明 + +* ここにAsiccDocを埋められないの? → 情報なし* +// 遅くなるので +* 比較モード時の差異があったとき、Diffを出す上限値をLimitDiffNumがほしい(デフォルトは各PDFで1pageとしたい) +* 比較NGになって、フルでDIFF画像を出したいときはLimitDiffを解除すべき。単独ファイル指定のとき。 + +// 設計の見直し(END) +#endif + + /// + /// PDF renderer main + /// + /// + /// + static int Main(string[] args) { + + // 引数の保持 + var pm = new ParamData(); + + bool bDirMode = false; // ディレクト指定の場合 + + //var pdfPathLst = new List(); + string[] pdfPathLst = null; + string[] pdfPathLst2 = null; + var pdfPathLstBoth = new List(); + var pdfPathLstNoBoth = new List(); + + //ハッシュ関係 + string hashBaseName = "RenderHash"; + CSRender.Check.GetDPI(); + + //{// ベースハッシュ値の計算テスト + // // 一旦使用しない + // var streamPDF = ResData.GetBaseHashPDF(); + // RenderPDF.RenderPdfInitCheck(streamPDF); + //} + + var qu = new Queue(args); // 引数をQueに登録 (qu.Enqueue(a)でも可能) + while (qu.Count > 0) { + // 引数のパース + var wd = qu.Dequeue(); // 取り出しqu.Dequeue()で次の要素を取得する + if (wd.First() == '-') { + // 先頭-(ハイフンもオプション扱いにする)→"/"に置換 + wd = Regex.Replace(wd, @"^\-", "/"); + } + //大文字小文字無視でオプションチェック + var eIgnoreCase = StringComparer.OrdinalIgnoreCase; + // オプションチェックローカル関数 + bool isOpt(params string[] opts) => opts.Contains(wd, eIgnoreCase); + // ボックスオプション辞書 + var BoxSelOptDic = new Dictionary(eIgnoreCase) { ["/BM"] = "Media", ["/BT"] = "Trim", + ["/BB"] = "Bleed", ["/BC"] = "Crop", + ["/BA"] = "Art" + }; + if (isOpt("/?", "/H")) { + DispHelp(); + return -1; + } else if (isOpt("/F")) { + pm.pdfPath = (qu.Count > 0) ? qu.Dequeue() : "";// next word. + } else if (isOpt("/O")) { + pm.outuptImageDir = (qu.Count > 0) ? qu.Dequeue() : "";// next word. + } else if (isOpt("/D")) { + pm.dpi = (qu.Count > 0) ? qu.Dequeue() : "";// next word. + if (!double.TryParse(pm.dpi, out double dmy)) { + Console.WriteLine($"解像度が不正です:/D {pm.dpi}"); + DispHelp(); + return -1; + } + } else if (isOpt("/Para")) { + string paraNum = (qu.Count > 0) ? qu.Dequeue() : "";// next word. + if (!int.TryParse(paraNum, out pm.para)) { + Console.WriteLine($"並行数が不正です:/para {paraNum}"); + DispHelp(); + return -1; + } + } else if (isOpt("/P")) { + pm.pageRange = (qu.Count > 0) ? qu.Dequeue() : "";// next word. + } else if (isOpt("/JPG", "/JPEG")) { + pm.imageType = "JPG"; + } else if (isOpt("/PNG")) { + pm.imageType = "PNG"; + } else if (isOpt("/TIF", "/TIFF")) { + pm.imageType = "TIFF"; + } else if (isOpt("/GIF", "/GIFF")) { + pm.imageType = "GIF"; + } else if (isOpt("/BMP")) { + pm.imageType = "BMP"; + } else if (isOpt("/JPEGQ")) { + pm.jpegQ = (qu.Count > 0) ? qu.Dequeue() : "";// next word. + if (!int.TryParse(pm.jpegQ, out int dmy)) { + Console.WriteLine($"JPEG Qualityが不正です:/JPEGQ {pm.jpegQ}"); + DispHelp(); + return -1; + } + if (!(0 < dmy && dmy <= 100)) { + Console.WriteLine($"JPEG Qualityが不正です(not 1-100):/JPEGQ {dmy}"); + return -1; + } + } else if (isOpt("/M")) { + pm.mode = (qu.Count > 0) ? qu.Dequeue() : "";// next word. + } else if (BoxSelOptDic.ContainsKey(wd)) { + pm.boxSelect = BoxSelOptDic[wd];// "/BT" -> "Trim",... + } else if (isOpt("/HASH")) { + pm.bHash = true; + } else if (isOpt("/MKHASH")) { + pm.bMkHash = true; + pm.bHash = true; + } else if (isOpt("/FC")) { + pm.bFC = true; + } else if (isOpt("/PDFium")) { + pm.bPDFium = true; + var flgStr = (qu.Count > 0) ? qu.Dequeue() : "";// next word. + if (!int.TryParse(flgStr, out int dmy)) { + Console.WriteLine($"PDFiumフラグが不正です:/PDFium {flgStr}"); + return -1; + } + if (dmy == 0) { + pm.bPDFium = false; + } else if (dmy == 1) { + pm.bPDFium = true; + } else { + Console.WriteLine($"PDFiumフラグが不正です:/PDFium {flgStr}"); + return -1; + } + } else if (isOpt("/Verbose")) { + var arg = (qu.Count > 0) ? qu.Dequeue() : "True";// next word. + string[] sel = {"True","1","ON"}; + pm.bVerbose = sel.Contains(arg, eIgnoreCase); + + + } else if (isOpt("/SubExe")) { + pm.subExe = (qu.Count > 0) ? qu.Dequeue() : null;// next word. + var sp = pm.subExe.Split(':'); + if (sp.Length < 2 ) { + Console.WriteLine($"SubExe指定は\":\"区切りが必要: {pm.subExe}"); + return -1; + } + } else if (isOpt("/NoExeSepa")) { + pm.bExeSepa = false; + } else if (isOpt("/RESULT")) { + pm.resultPath = (qu.Count > 0) ? qu.Dequeue() : "";// next word. + } else if (wd.First() == '/') { + // 処理の無いオプションを明示的に無視する + Console.WriteLine($"Warning::Ignore opt:{wd}"); + } else { + // 引数をファイル名として取得する + if (pm.pdfPath == "") { + pm.pdfPath = wd; + continue; + } + if (pm.bFC && (pm.pdfPath2 == "")) { + pm.pdfPath2 = wd;// 比較時のみ取得 + continue; + } + } + } + // ↑引数解析終わり + setEcho(pm.bVerbose); + if (pm.bVerbose) + echo("Varbose Mode!"); + // + bool bSubExe = (pm.subExe != null);// SubExeで起動されている場合。 + xChangeWCFPipe.IXData cltSrv = null; + if (!bSubExe) { + /// サーバー側 + echo("++MainProcess start"); + } else { + // SubExe側の動作に差し替える + var sp = pm.subExe.Split(':'); + var pipeName = sp[0]; var pipeAddr = sp[1]; + cltSrv = xChangeWCFPipeGEN.CLT_T.makeCLT(pipeName:pipeName,pipeAddress:pipeAddr); + Ut.Ut.SetAutoSelfKillByMainProcEnd(); // 起動元が終了した場合に自身を終了させる + pm = cltSrv.GetParam(); // パラメータを書き換える + setEcho(pm.bVerbose); + echo("++SubProcess start:" + pm.subExe); + } + + if (pm.pdfPath == "") { + Console.WriteLine("pdfファイルが指定されてません"); + DispHelp(); + return -1; + } + if (!(File.Exists(pm.pdfPath) || Directory.Exists(pm.pdfPath))) { + // ファイルもしくはディレクトも見つからない場合 + Console.WriteLine($"ファイルが存在しません:{pm.pdfPath}"); + return -1; + } + var rdCond = new RenderPDF.RenderConditionParams { + Dpi = double.Parse(pm.dpi), + ImageType = pm.imageType, + BoxType = pm.boxSelect, + JpegQ = int.Parse(pm.jpegQ) + }; + bool bDir = File.GetAttributes(pm.pdfPath).HasFlag(FileAttributes.Directory); + if (bDir) { + // Directoryが指定されたのでファイルリストアップ + bDirMode = true; + pdfPathLst = System.IO.Directory.GetFiles(pm.pdfPath, "*.pdf"/*, System.IO.SearchOption.AllDirectories*/); + pdfPathLst = Array.ConvertAll(pdfPathLst, f => Path.GetFileName(f)); // 配列書き換え(ファイル名のみにする + // var enumLst = pdfPathLst.Select(f => Path.GetFileName(f)); LINQ式に置き換えることも可能(返り値は配列ではない) + //foreach ( var f in pdfPathLst) { + // Console.WriteLine($@"path1={f}"); + //} + //Console.WriteLine($@"path1.Len={pdfPathLst.Count()}"); + } + + + var resultData = new SortedDictionary(); + + if ((!pm.bFC) && bDir) { + // 単純レンダリング時 かつ ディレクト時に 対象リストに追加 + foreach (var f in pdfPathLst) { + pdfPathLstBoth.Add(f); + } + } + if (pm.bFC) {// 比較モード時 + if (pm.pdfPath2 == "") { + Console.WriteLine("比較モードで2つ目のpdfファイルが指定されてません"); + return -1; + } + if (!(File.Exists(pm.pdfPath2) || Directory.Exists(pm.pdfPath2))) { + Console.WriteLine($"比較ファイルが存在しません:{pm.pdfPath2}"); + return -1; + } + bool bDir2 = File.GetAttributes(pm.pdfPath2).HasFlag(FileAttributes.Directory); + if (bDir2) { + if (!bDir) { + Console.WriteLine($"比較対象はファイルパスでないといけません:{pm.pdfPath2}"); + return -1; + } + // 2つ目のファイルリストアップ + pdfPathLst2 = System.IO.Directory.GetFiles(pm.pdfPath2, "*.pdf"/*, System.IO.SearchOption.AllDirectories*/); + pdfPathLst2 = Array.ConvertAll(pdfPathLst2, f => Path.GetFileName(f));// 配列の書き換え + + // 共通のファイルを見つける。Lstの要素がLst2に含まれているかどうか + foreach (var f in pdfPathLst) { + if (pdfPathLst2.Contains(f)) { + pdfPathLstBoth.Add(f); + } else { + pdfPathLstNoBoth.Add(f);// LstがLst2に含まれていない + } + } + // Lst2のみファイルをNoBothに登録 + foreach (var f in pdfPathLst2) { + if (!pdfPathLstBoth.Contains(f)) { + pdfPathLstNoBoth.Add(f); + } + } + // pdfPathBoth,pdfPathNoBothが作成済み + if (pdfPathLstNoBoth.Count() != 0) { + Console.WriteLine($@"不一致のファイル={pdfPathLstNoBoth.Count()}"); + foreach (var f in pdfPathLstNoBoth) { + Console.WriteLine($@"warning [no match]={f}"); + } + } + } + } + var watch = System.Diagnostics.Stopwatch.StartNew(); // 時間の生成と計測開始を同時に行う + var otDir = pm.outuptImageDir; + if (otDir == "") { + // 出力ディレクトリの作成 // 元PDFの同一フォルダにIMGフォルダを作成する + otDir = Path.Combine(Directory.GetParent(pm.pdfPath).FullName, "IMG"); + } + + if (!Directory.Exists(otDir) /* && (!bMkHash)*/ ) { // MkHashのとき不要→必要 + Directory.CreateDirectory(otDir); + } + var otHashPath = ""; + var otHashPath2 = ""; + int ret = -1; + //Console.WriteLine($@"Use bPDFium={pm.bPDFium}"); + + if (!bDirMode) { + if (pm.bFC) { + //比較モード + // ハッシュデータの読み込み + echo($@"pre pdfPath={pm.pdfPath}"); + echo($@"pre pdfPath2={pm.pdfPath2}"); + + var hashDataTgt = HashData.load(pm.pdfPath, hashBaseName, pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ, ref otHashPath); + var hashDataRef = HashData.load(pm.pdfPath2, hashBaseName, pm.dpi, pm.boxSelect, pm.imageType, pm.jpegQ, ref otHashPath2); + //Console.WriteLine(""); + + if (pm.resultPath != "") {// 初期化 + File.WriteAllText(Path.GetFullPath(pm.resultPath), ""); + } + + ret = RenderPDF.RenderPdfDocCompare( + pdfPath: Path.GetFullPath(pm.pdfPath), + refPdfPath: Path.GetFullPath(pm.pdfPath2), + resultDataPath: pm.resultPath, + inDir : otDir, + pm : rdCond, + inPageRange : pm.pageRange, + inHashDataTgt: hashDataTgt, + inHashDataRef: hashDataRef, + bPDFium : pm.bPDFium + ); + } else { + // Sync version. + // Console.WriteLine(""); + // シングル指定では既存のハッシュファイルを読みださない → 単独名のハッシュファイル");[ + var count = pdfPathLstBoth.Count; + + //ParallelOptions options = new ParallelOptions { MaxDegreeOfParallelism = 6 }; + //Parallel.For(0,pdfPathLstBoth.Count, options, index => { + //逆に遅くなる UWPコール(render)は対応していない? + if (pm.bExeSepa &&(!pm.bFC)) { // FCモードは除外します。 + var tokenSource = new CancellationTokenSource(); + ParallelOptions options = new ParallelOptions { MaxDegreeOfParallelism = pm.para }; + ret = 0;//Success + var loopResult = Parallel.For(0,pdfPathLstBoth.Count, options, (index,lpState) => { + //for (var index = 0; index < pdfPathLstBoth.Count; index++) { + var tgt = Path.Combine(pm.pdfPath, pdfPathLstBoth[index]); + var tgtID = $@"{index}/{pdfPathLstBoth.Count}"; + + Console.WriteLine($@"**Remain({count})****tgt({tgtID})={tgt}"); + if (pm.bFC) { + //比較モード + var reff = Path.Combine(pm.pdfPath2, pdfPathLstBoth[index]); + } else { // Sync version(Paralles). + var svrData = new xChangeWCFPipe.XData(); + var pmClone = pm.Clone(); + pmClone.pdfPath = tgt;// 引数のターゲットのみを書き換える + svrData.SetParam(pmClone); + string guidStr = Guid.NewGuid().ToString("N"); + string pipeName = $"PN_{guidStr}"; + string pipeAddress = $"P_{index}_{guidStr}"; + + var svrHost = xChangeWCFPipeGEN.SVR_T.makeSVR( + baseIf : typeof(xChangeWCFPipe.IXData), + obj : svrData, + pipeName : pipeName, + pipeAddress : pipeAddress + ); + if ( svrHost == null) { + Console.WriteLine("WCF cannot make"); + //tokenSource.Cancel();// 処理のキャンセル( throwされる) + lpState.Stop(); + return; + } + var myExePath = Assembly.GetExecutingAssembly().Location; + var proc = Ut.Ut.DoCmd( + cmdAndArgs: new string[] { myExePath, "/SubExe",$"{pipeName}:{pipeAddress}" } + , bEcho: false + , bSameConsole: true + ); + proc.WaitForExit(); + var retProc = proc.ExitCode; + echo($@"retProc={retProc}"); + if ( retProc < 1 ) { + //PDFページ数以外が返った時 + ret = -1; + Console.WriteLine( $@"Warning ::Cannot get PDFPage:{retProc}"); + } else { + ;// 初期値 ret=0; + } + //while (!proc.HasExited) { + // System.Threading.Thread.Sleep(2 * 1000); + //} + var tmpH = svrData.GetHashData(); + if (tmpH == null) { + echo("tmpH is null"); + } + if ( (hashDataTgt!=null)&&(tmpH!=null)) { + lock(hashDataTgt) { + //hashDataTgt.Files += tmpH.Files; + if ( tmpH.Files.Count() < 1 ) {// 中身が空なら警告 + Console.WriteLine($@"Warning tempF.Count={tmpH.Files.Count()}"); + } + var merged = hashDataTgt.Files + .Concat(tmpH.Files.Where(pair => + !hashDataTgt.Files.ContainsKey(pair.Key)) + ).ToDictionary( + pair => pair.Key, + pair => pair.Value + ); + hashDataTgt.Files = new SortedDictionary(merged);// hashを合成した。 + } + } + proc.Dispose(); + svrHost.Close(); + svrData = null; + Console.WriteLine($@"End tgt({tgtID})"); + } + //GC.Collect();// これでメモリリークが解決した Paraの中ではやめておく + count--; + }); + if ( !loopResult.IsCompleted ) { + Console.WriteLine("中断"); + pm.bMkHash = false;// Hash値保存抑制 + } + ////tokenSource.Cancel();// 処理のキャンセル + //if ( tokenSource.IsCancellationRequested ) { + // pm.bMkHash = false;// Hash値保存抑制 + //} + //}; // Non Para Block + } else { // NoExeSepa + + if (pm.resultPath != "") {// 初期化 + File.WriteAllText(Path.GetFullPath(pm.resultPath), ""); + } + for (var index = 0; index < pdfPathLstBoth.Count; index++) { + var tgt = Path.Combine(pm.pdfPath, pdfPathLstBoth[index]); + Console.WriteLine($@"tgt={index+1}/{pdfPathLstBoth.Count}:{tgt}"); + if (pm.bFC) { + //比較モード + var reff = Path.Combine(pm.pdfPath2, pdfPathLstBoth[index]); + //Console.WriteLine(""); + ret = RenderPDF.RenderPdfDocCompare( + pdfPath: Path.GetFullPath(tgt), + refPdfPath: Path.GetFullPath(reff), + resultDataPath: pm.resultPath, + inDir: otDir, + pm: rdCond, + inPageRange: pm.pageRange, + inHashDataTgt: hashDataTgt, + inHashDataRef: hashDataRef, + bPDFium: pm.bPDFium + ); + } else { // Sync version(Paralles). + ret = RenderPDF.RenderPdfDoc( + pdfPath: Path.GetFullPath(tgt), + inDir: otDir, + pm: rdCond, + inPageRange: pm.pageRange, + bSaveImage: pm.bMkHash ? false : true,// ハッシュ値生成ではイメージ保存しない。 + bHash: pm.bHash,// bHash,dumy + inHashData: pm.bMkHash ? hashDataTgt : null, //ハッシュコマンドモードのみDataを渡す + bPDFium: pm.bPDFium + ); + } + count--; + GC.Collect();// これでメモリリークが解決した + }; + } + if (pm.bMkHash) { + hashDataTgt.save(otHashPath);//ハッシュモードで保存する(Dutyチェックもいるでしょう + } + } + watch.Stop(); + Console.WriteLine($"result={ret},time={ watch.ElapsedMilliseconds / 1000.0 }[sec]"); +#if COMMNET + Assembly assm = Assembly.GetExecutingAssembly(); + Console.WriteLine(assm.FullName); + // 参照しているすべてのアセンブリを取得し、表示する + foreach (AssemblyName refassm in assm.GetReferencedAssemblies()) { + Console.WriteLine($@" {refassm.FullName},Ver({refassm.Version}),VerComp({refassm.VersionCompatibility})" ); + } + string clrVersion = System.Environment.Version.ToString(); + Console.WriteLine($@"clrVer={clrVersion}"); +#endif + return ret;//success. + } + + // リソースからの取得 + public static class ResData { + public static Stream GetBaseHashPDF(string resName = "CSRender.RES.BaseHash.pdf") { + System.Reflection.Assembly asm = System.Reflection.Assembly.GetExecutingAssembly(); + var sel = from x in asm.GetManifestResourceNames() select resName; + if (sel.Count() == 1) { + return asm.GetManifestResourceStream(sel.First()); + } + return null; + } + } + + } +} +/// +/// WCF通信モジュールのラッパクラス。 +/// 2つのExe間をWCFパイプモードで通信を行う +/// +namespace xChangeWCFPipe +{ + /// シリアライズ可能にすること + /// + [ServiceContract] + public interface IXData { + [OperationContract(IsOneWay = true)] + void Execute(); + + [OperationContract] + bool SetMessage(string msg); + + [OperationContract] + bool SetProgress(int per);//0-100. + + [OperationContract] + HashData GetHashData(); + + [OperationContract] + void SetHashData(HashData d); + + [OperationContract] + CSRenderMain.ParamData GetParam(); + //CSRenderMain.Program.ParamData GetParam(); + } + // ServiceContract https://tnakamura.hatenablog.com/entry/20080606/1220023868 + // https://devlights.hatenablog.com/entry/20111023/p2 + + /// + /// 通信用複雑データ。スカラー型(int,double,,,.)以外は[DataCOntract]属性を + /// つけて、通知用Interfaceの引数や、返値で利用する + /// + [DataContract] + public class DataContainer + { + [DataMember] + public CSRenderMain.ParamData pm; + [DataMember] + public HashData hdata; + } +} + +namespace xChangeWCFPipe +{ + /// + /// Main(サーバー)側 データ定義 + /// + [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple, UseSynchronizationContext = false)] + public class XData : IXData + { + private readonly Object LockObj = new object();// 排他制御用 + public delegate void callFunc(); + public callFunc callDelegate; + public XData() { + _dc.pm = new CSRenderMain.ParamData(); + } + public void Execute() { + lock (LockObj) { + callDelegate(); // 実装はデリゲートします + } + } + public bool SetMessage(string msg) { + lock (LockObj) { + Console.WriteLine($"SVR:called setMessage({msg}) from CLT"); + _msg = msg; + } + return true; + } + public bool SetProgress(int per) {//0-100. + lock (LockObj) { + Console.WriteLine($"SVR:called setProgress({per}) from CLT"); + _progress = per; + } + return true; + } + + public void SetHashData(HashData h) { + lock (LockObj) { + _dc.hdata = h; + } + } + public HashData GetHashData() { + lock (LockObj) { + return _dc.hdata; + } + } + public CSRenderMain.ParamData GetParam() { + lock (LockObj) { + return _dc.pm; + } + } + // クライアントに公開しなくてもよいI/F + public void SetParam(CSRenderMain.ParamData pm) { + lock (LockObj) { + _dc.pm = pm; + } + } + // private data + private string _msg = "init msg"; + private int _progress = 0;// 0-100 + private DataContainer _dc = new DataContainer(); + } +} + +namespace xChangeWCFPipeGEN +{ + /// + /// Main(サーバー)側 データ定義 + /// + //[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple, UseSynchronizationContext = false)] + //public class XData : IXData + //{ + // ... + //} + /// + /// Main(サーバー)側クラス + /// + /// + public class SVR_T + { + // - static - + + /// + /// + /// + /// 公開するインターフェースクラス + /// 実装インスタンス(baseIfを含むこと) + /// + /// + /// + static public SVR_T makeSVR(Type baseIf, object obj/*XData*/ , string pipeName = "PDFormstudioESorGS", string pipeAddress = "SubModuleAddress") { + + Type oType = obj.GetType(); + bool hasBaseIf = oType.GetInterfaces().Any(t => (t == baseIf) ); + if ( !hasBaseIf) { + Console.WriteLine($@"Error:'{oType.FullName}' have not the interface '{baseIf.FullName}'");//A have not the interface B. + return null; + } + return (new SVR_T(baseIf, obj, pipeName, pipeAddress)); + } + // - public I/F - + public void Close() { + echo($"Close Service:{_service != null},{_serviceHost != null}"); + _service = null; + _serviceHost.Close(); + } + // - private - + private object _service = null;// 要らないかも + private Type _baseIf = null; + private ServiceHost _serviceHost = null; + private SVR_T() { } + private SVR_T(Type baseIf, object oInstance, string pipeName, string pipeAddress) { + const string pipeBase = "net.pipe://localhost"; + var uri = pipeBase + "/" + pipeName; + _baseIf = baseIf; + _service = oInstance; + // デリゲート登録 + //callDelegate = () => { Console.WriteLine("svr:Delegate func called"); } +#if false + { + // インターフェースを求める + // AddServiceEndpoint()にわたすIXDataはXData.Interfaces()から求められるが + // インタフェースは一つとは限らないのでやめておく。 + Type interfType = null; + Type type = _service.GetType(); + Type[] interfaces = type.GetInterfaces(); + Console.WriteLine($@"GetInterfaces***********************************"); + foreach (Type t in interfaces) { + Console.WriteLine(t.ToString()); + interfType = t; + }; + Console.WriteLine($@"End of GetInterfaces***********************************"); + } +#endif + _serviceHost = new ServiceHost(_service, new Uri(uri)); + try { + _serviceHost.AddServiceEndpoint(_baseIf/*typeof(Ti)*/, new NetNamedPipeBinding(), pipeAddress); + _serviceHost.Open(); + } catch (AddressAlreadyInUseException) { + Console.WriteLine("既にサービスは起動しています。"); + } catch (Exception e) { + Console.WriteLine(e.ToString()); + } + } + } + /// + /// Sub(クライアント)側クラス + /// + public class CLT_T + { + /// + /// Sub側の初期化処理。Main側とpipeName,pipeAddressを同じにすること + /// + /// + /// + /// + static public Ti makeCLT(string pipeName = "PDFormstudioESorGS", string pipeAddress = "SubModuleAddress") { + const string pipeBase = "net.pipe://localhost"; + var address = pipeBase + "/" + pipeName + "/" + pipeAddress; + //try { + var factory = (new ChannelFactory(new NetNamedPipeBinding(), new EndpointAddress(address))); + Ti toMain = factory.CreateChannel(); + return toMain; + //} catch (Exception e) { + // Console.WriteLine("makeCLT failed:" + e.ToString()); + // return null; + //} + } + } +} + +namespace Ut +{ + public class Ut + { + // Util + // -------------------------------------------------------------- + /// + /// 親プロセスが終了したら、自身を終了させる + /// + /// + public static void SetAutoSelfKillByMainProcEnd(bool bEnable = true) { + if (_SingletonProc != null) + return;//一度しか呼び出せない + if (bEnable == false) + return; + const string fname = "SetAutoSelfKillByMainProcEnd"; + // 自動的に親のプロセスがいなくなったら自動的にキルモードを設定する + // メインプロセスの終了チェック + // 親プロセスIDでProcessハンドルを取得、そのExitedイベントに自身の終了関数を設定(Environment.Exit()) + int paProcID = GetParentProcessId(); + var paProc = Process.GetProcessById(paProcID); + paProc.EnableRaisingEvents = true; + //Console.WriteLine($@"{fname}:ParrentProcessName ={paProc.ProcessName}({paProcID})"); + paProc.Exited += new EventHandler( + (object s, EventArgs a) => { + Console.WriteLine($@"{fname}:Exited Event!!!!"); + var ss = s as Process; + Console.WriteLine($@"Sure ProcName is {ss.ProcessName}({ss.Id}) {ss.StartInfo.Arguments}"); ; + System.Threading.Thread.Sleep(10 * 1000); + ss.Close(); + ss.Dispose(); + Console.WriteLine($@"{fname}:Exited Event!!!!(afer10sec)"); + //Environment.Exit(-1); + }); + // + _SingletonProc = paProc; + } + private static System.Diagnostics.Process _SingletonProc = null; + /// + /// コマンドライン引数複数個をエンコードして、スペースで結合 + /// + /// string[] コマンドライン引数 + /// コマンドライン文字列(Escaped) + public static string makeCmdLine(IEnumerable args) { + if (args == null) + throw new ArgumentNullException("args"); + string EscapeCmdLineArg(string v) { + if (string.IsNullOrEmpty(v)) return ""; + var containsSpace = v.IndexOfAny(new[] { ' ', '\t' }) != -1; + v = ReCommandLineEscapePattern.Replace(v, @"$1\$&");//「\…\"」をエスケープ.「"」直前の「\」の数を 2倍+1 + if (containsSpace) { + v = "\"" + ReLastBackSlashPattern.Replace(v, "$1$1") + "\""; + } + return v; + } + return string.Join(" ", args.Select(v => EscapeCmdLineArg(v))); + } + private static Regex ReCommandLineEscapePattern = new Regex("(\\\\*)\""); + private static Regex ReLastBackSlashPattern = new Regex(@"(\\+)$"); + + /// + /// 親のプロセスIDを取得する + /// + /// 親ProcessID + static int GetParentProcessId() { + var myProcId = GetCurrentProcessId(); + var query = string.Format($@"SELECT ParentProcessId FROM Win32_Process WHERE ProcessId = {myProcId}"); + //クエリから結果を取得 + using (var search = new System.Management.ManagementObjectSearcher(@"root\CIMV2", query)) + using (var results = search.Get().GetEnumerator()) { + if (!results.MoveNext()) + throw new ApplicationException("Couldn't Get ParrentProcessId."); + var queryResult = results.Current; + //親プロセスのPIDを取得 + uint pa = (uint)(queryResult["ParentProcessId"]); + return (int)(pa); + } + } + /// + /// 自身のプロセスIDを取得する + /// + /// ProcessID + static int GetCurrentProcessId() { return Process.GetCurrentProcess().Id; } + /// + /// 外部プログラムを起動する + /// + /// 引数文字列の配列 + /// true:コンソールを呼び出しと共有 false:別Console Windows + /// 標準出力(未実装) + /// コマンドが終了するまで待つ(false:未実装,常に待つ) + /// タイムアウト時間ms(未実装) + /// + public static Process DoCmd(IEnumerable cmdAndArgs, bool bSameConsole = true, bool bEcho = true, bool bWait = false, int nWaitLimitMSec = -1) { + var cmdName = cmdAndArgs.First();//[0]; + var argStr = makeCmdLine(cmdAndArgs.Skip(1));// 先頭を除外してコピー new ArraySegment(cmdAndArgs.ToArray(), 1, cmdAndArgs.Count()-1) + //Console.WriteLine($@"DoCmd:{cmdName}:args[{argStr}]"); + //using (var p = new Process()) { + var p = new Process(); + { + var info = p.StartInfo; + p.StartInfo.FileName = cmdName; + p.StartInfo.Arguments = argStr; + if (bSameConsole) { + //Console.WriteLine("Same windows mode"); + p.StartInfo.UseShellExecute = false; // Shell経由で実行しない(必須) true: 別ウインドウ。false:コンソールを共有 + p.StartInfo.CreateNoWindow = false; // ウィンドウを作成しない + } else { + //Console.WriteLine("New windows mode"); + p.StartInfo.UseShellExecute = true; + p.StartInfo.CreateNoWindow = true; + } + p.Start();// 実行 + if (bWait == false) { + return p; + } + if (nWaitLimitMSec == -1) { + p.WaitForExit();// タイムアウト無し + } else { + bool bExit = p.WaitForExit(nWaitLimitMSec); + if (!bExit) {// 処理の終了を待つ + Console.WriteLine("処理が終了しないので強制終了"); + p.Kill(); // 強制終了する + return p; + } + } + return p; + } + } + } +} \ No newline at end of file diff --git a/bin/Release/CSRender.exe b/bin/Release/CSRender.exe index 36dd24cbf49a0e0e8eebf3d767277569398ac7a1..620d841505545f45132fb8a918713f5b8b78bb3b 100644 GIT binary patch delta 25575 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 YuX~hrEz&518zJ}U$p;>4k>*AIKc_g3CIA2c delta 36398 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + -- 2.22.0