From 7cf4eedcc98695d2826e04a9c3326427b811ab2d Mon Sep 17 00:00:00 2001 From: Kagamia Date: Sun, 5 Jan 2025 09:16:36 +0800 Subject: [PATCH 1/7] Common: refactor gif encoder, add ffmpeg encoder. #273 --- WzComparerR2.Avatar/UI/AvatarForm.cs | 42 +- .../{ => Encoders}/BuildInApngEncoder.cs | 44 +- .../{ => Encoders}/BuildInGifEncoder.cs | 43 +- WzComparerR2.Common/Encoders/FFmpegEncoder.cs | 172 +++++++ .../{ => Encoders}/GifEncoder.cs | 30 +- .../Encoders/GifEncoderCompatibility.cs | 30 ++ .../{ => Encoders}/IndexGifEncoder.cs | 35 +- WzComparerR2.Common/Gif.cs | 7 +- WzComparerR2/AnimateEncoderFactory.cs | 148 ++++--- WzComparerR2/Config/ImageHandlerConfig.cs | 21 + WzComparerR2/FrmGifSetting.Designer.cs | 419 +++++++++++++++--- WzComparerR2/FrmGifSetting.cs | 62 ++- WzComparerR2/MainForm.cs | 47 +- WzComparerR2/PictureBoxEx.cs | 68 +-- 14 files changed, 907 insertions(+), 261 deletions(-) rename WzComparerR2.Common/{ => Encoders}/BuildInApngEncoder.cs (58%) rename WzComparerR2.Common/{ => Encoders}/BuildInGifEncoder.cs (73%) create mode 100644 WzComparerR2.Common/Encoders/FFmpegEncoder.cs rename WzComparerR2.Common/{ => Encoders}/GifEncoder.cs (62%) create mode 100644 WzComparerR2.Common/Encoders/GifEncoderCompatibility.cs rename WzComparerR2.Common/{ => Encoders}/IndexGifEncoder.cs (61%) diff --git a/WzComparerR2.Avatar/UI/AvatarForm.cs b/WzComparerR2.Avatar/UI/AvatarForm.cs index a93a50cb..dee2dc8d 100644 --- a/WzComparerR2.Avatar/UI/AvatarForm.cs +++ b/WzComparerR2.Avatar/UI/AvatarForm.cs @@ -987,14 +987,18 @@ private void btnSaveAsGif_Click(object sender, EventArgs e) } else { + // get default encoder var config = ImageHandlerConfig.Default; - var encParams = AnimateEncoderFactory.GetEncoderParams(config.GifEncoder.Value); + using var encoder = AnimateEncoderFactory.CreateEncoder(config); + var cap = encoder.Compatibility; + + string extensionFilter = string.Join(";", cap.SupportedExtensions.Select(ext => $"*{ext}")); var dlg = new SaveFileDialog() { Title = "Save avatar", - Filter = string.Format("{0}(*{1})|*{1}|All files(*.*)|*.*", encParams.FileDescription, encParams.FileExtension), - FileName = string.Format("avatar{0}", encParams.FileExtension) + Filter = string.Format("{0} Supported Files ({1})|{1}|All files (*.*)|*.*", encoder.Name, extensionFilter), + FileName = string.Format("avatar{0}", cap.DefaultExtension) }; if (dlg.ShowDialog() != DialogResult.OK) @@ -1002,6 +1006,7 @@ private void btnSaveAsGif_Click(object sender, EventArgs e) return; } + string outputFileName = dlg.FileName; var actPlaying = new[] { bodyPlaying, emoPlaying, tamingPlaying }; var actFrames = new[] { cmbBodyFrame, cmbEmotionFrame, cmbTamingFrame } .Select((cmb, i) => @@ -1026,7 +1031,7 @@ private void btnSaveAsGif_Click(object sender, EventArgs e) var gifLayer = new GifLayer(); - if (aniCount == 1) + if (aniCount == 1 && !cap.IsFixedFrameRate) { int aniActIndex = Array.FindIndex(actPlaying, b => b); for (int fIdx = 0, fCnt = actFrames[aniActIndex].Length; fIdx < fCnt; fIdx++) @@ -1054,8 +1059,8 @@ private void btnSaveAsGif_Click(object sender, EventArgs e) else { // more than 2 animating action parts, for simplicity, we use fixed frame delay. - var aniLength = actFrames.Max(layer => layer == null ? 0 : layer.Sum(f => f.actionFrame.AbsoluteDelay)); - var aniDelay = 30; + int aniLength = actFrames.Max(layer => layer == null ? 0 : layer.Sum(f => f.actionFrame.AbsoluteDelay)); + int aniDelay = config.MinDelay; // pipeline functions IEnumerable RenderDelay() @@ -1150,7 +1155,7 @@ GifFrame ApplyFrame(int[] actionIndices, int delay) // build pipeline var step1 = RenderDelay(); var step2 = GetFrameActionIndices(step1); - var step3 = MergeFrames(step2); + var step3 = cap.IsFixedFrameRate ? step2 : MergeFrames(step2); var step4 = step3.Select(tp => ApplyFrame(tp.Item1, tp.Item2)); // run pipeline @@ -1202,27 +1207,24 @@ Brush CreateBackgroundBrush() } } - var bgBrush = CreateBackgroundBrush(); - using (var enc = AnimateEncoderFactory.CreateEncoder(dlg.FileName, clientRect.Width, clientRect.Height, config)) + using var bgBrush = CreateBackgroundBrush(); + encoder.Init(outputFileName, clientRect.Width, clientRect.Height); + foreach (IGifFrame gifFrame in gifLayer.Frames) { - foreach (IGifFrame gifFrame in gifLayer.Frames) + using (var bmp = new Bitmap(clientRect.Width, clientRect.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb)) { - using (var bmp = new Bitmap(clientRect.Width, clientRect.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb)) + using (var g = Graphics.FromImage(bmp)) { - using (var g = Graphics.FromImage(bmp)) + // draw background + if (bgBrush != null) { - // draw background - if (bgBrush != null) - { - g.FillRectangle(bgBrush, 0, 0, bmp.Width, bmp.Height); - } - gifFrame.Draw(g, clientRect); + g.FillRectangle(bgBrush, 0, 0, bmp.Width, bmp.Height); } - enc.AppendFrame(bmp, Math.Max(10, gifFrame.Delay)); + gifFrame.Draw(g, clientRect); } + encoder.AppendFrame(bmp, Math.Max(cap.MinFrameDelay, gifFrame.Delay)); } } - bgBrush?.Dispose(); } } diff --git a/WzComparerR2.Common/BuildInApngEncoder.cs b/WzComparerR2.Common/Encoders/BuildInApngEncoder.cs similarity index 58% rename from WzComparerR2.Common/BuildInApngEncoder.cs rename to WzComparerR2.Common/Encoders/BuildInApngEncoder.cs index 8fce1a1c..5aa52831 100644 --- a/WzComparerR2.Common/BuildInApngEncoder.cs +++ b/WzComparerR2.Common/Encoders/BuildInApngEncoder.cs @@ -5,27 +5,43 @@ using System.Drawing; using System.Runtime.InteropServices; -namespace WzComparerR2.Common +namespace WzComparerR2.Encoders { public class BuildInApngEncoder : GifEncoder { - public BuildInApngEncoder(string fileName, int width, int height) - : base(fileName, width, height) + public BuildInApngEncoder() { - var err = apng_init(fileName, width, height, out this.handle); + } + + public bool OptimizeEnabled { get; set; } + + private IntPtr handle; + + public override GifEncoderCompatibility Compatibility => new GifEncoderCompatibility() + { + IsFixedFrameRate = false, + MinFrameDelay = 1, + MaxFrameDelay = 655350, + FrameDelayStep = 1, + AlphaSupportMode = AlphaSupportMode.FullAlpha, + DefaultExtension = ".png", + SupportedExtensions = new[] { ".png" }, + }; + + public override void Init(string fileName, int width, int height) + { + base.Init(fileName, width, height); + + var err = apng_init(fileName, width, height, out handle); if (err != ApngError.Success) { throw new Exception($"Apng error: {err}."); } } - private IntPtr handle; - - public bool OptimizeEnabled { get; set; } - public override void AppendFrame(IntPtr pBuffer, int delay) { - var err = apng_append_frame(this.handle, pBuffer, 0, 0, this.Width, this.Height, this.Width * 4, delay, this.OptimizeEnabled); + var err = apng_append_frame(handle, pBuffer, 0, 0, Width, Height, Width * 4, delay, OptimizeEnabled); if (err != ApngError.Success) { throw new Exception($"Apng error: {err}."); @@ -36,8 +52,12 @@ protected override void Dispose(bool disposing) { if (disposing) { - apng_write_end(this.handle); - apng_destroy(ref this.handle); + if (handle != IntPtr.Zero) + { + apng_write_end(handle); + apng_destroy(ref handle); + handle = IntPtr.Zero; + } } base.Dispose(disposing); } @@ -52,7 +72,7 @@ enum ApngError : int }; [DllImport("libapng.dll")] - static extern ApngError apng_init([MarshalAs(UnmanagedType.LPWStr)]string fileName, int width, int height, out IntPtr ppEnc); + static extern ApngError apng_init([MarshalAs(UnmanagedType.LPWStr)] string fileName, int width, int height, out IntPtr ppEnc); [DllImport("libapng.dll")] static extern ApngError apng_append_frame(IntPtr pEnc, IntPtr pData, int x, int y, int width, int height, int stride, int delay_ms, bool optimize); [DllImport("libapng.dll")] diff --git a/WzComparerR2.Common/BuildInGifEncoder.cs b/WzComparerR2.Common/Encoders/BuildInGifEncoder.cs similarity index 73% rename from WzComparerR2.Common/BuildInGifEncoder.cs rename to WzComparerR2.Common/Encoders/BuildInGifEncoder.cs index 18cdd559..a419de91 100644 --- a/WzComparerR2.Common/BuildInGifEncoder.cs +++ b/WzComparerR2.Common/Encoders/BuildInGifEncoder.cs @@ -7,20 +7,27 @@ using System.Drawing.Imaging; using ImageManipulation; -namespace WzComparerR2.Common +namespace WzComparerR2.Encoders { public class BuildInGifEncoder : GifEncoder { - public BuildInGifEncoder(string fileName, int width, int height) - : base(fileName, width, height) + public BuildInGifEncoder() { - bWriter = new BinaryWriter(File.Create(fileName)); mStream = new MemoryStream(); quantizer = new OctreeQuantizer(255, 8); - - WriteHeader(); } + public override GifEncoderCompatibility Compatibility => new GifEncoderCompatibility() + { + IsFixedFrameRate = false, + MinFrameDelay = 10, + MaxFrameDelay = 655350, + FrameDelayStep = 10, + AlphaSupportMode = AlphaSupportMode.OneBitAlpha, + DefaultExtension = ".gif", + SupportedExtensions = new[] { ".gif" }, + }; + private BinaryWriter bWriter; private MemoryStream mStream; private Quantizer quantizer; @@ -32,6 +39,14 @@ public BuildInGifEncoder(string fileName, int width, int height) 0x03,0x01,0x00,0x00,0x00};//循环信息 其他信息 private static readonly byte[] gifEnd = new byte[] { 0x3b };//结束信息 + public override void Init(string fileName, int width, int height) + { + base.Init(fileName, width, height); + + bWriter = new BinaryWriter(File.Create(fileName)); + WriteHeader(); + } + public override void AppendFrame(Bitmap image, int delay) { mStream.SetLength(0); @@ -43,12 +58,12 @@ public override void AppendFrame(Bitmap image, int delay) byte[] tempArray = mStream.GetBuffer(); // 781开始为Graphic Control Extension块 标志为21 F9 04 - tempArray[784] = (byte)0x09; //图像刷新时屏幕返回初始帧 貌似不打会bug 意味不明 测试用 + tempArray[784] = 0x09; //图像刷新时屏幕返回初始帧 貌似不打会bug 意味不明 测试用 delay = delay / 10; tempArray[785] = (byte)(delay & 0xff); tempArray[786] = (byte)(delay >> 8 & 0xff); //写入2字节的帧delay // 787为透明色索引 788为块结尾0x00 - tempArray[787] = (byte)0xff; + tempArray[787] = 0xff; // 789开始为Image Descriptor块 标志位2C // 790~793为帧偏移大小 默认为0 // 794~797为帧图像大小 默认他 @@ -62,9 +77,9 @@ public override void AppendFrame(Bitmap image, int delay) public override void AppendFrame(IntPtr pBuffer, int delay) { - using(var bmp = new Bitmap(Width, Height, Width *4, PixelFormat.Format32bppArgb, pBuffer)) + using (var bmp = new Bitmap(Width, Height, Width * 4, PixelFormat.Format32bppArgb, pBuffer)) { - this.AppendFrame(bmp, delay); + AppendFrame(bmp, delay); } } @@ -83,8 +98,12 @@ protected override void Dispose(bool disposing) { if (disposing) { - bWriter.Write(gifEnd); - bWriter.Close(); + if (bWriter != null) + { + bWriter.Write(gifEnd); + bWriter.Close(); + bWriter = null; + } } if (mStream != null) diff --git a/WzComparerR2.Common/Encoders/FFmpegEncoder.cs b/WzComparerR2.Common/Encoders/FFmpegEncoder.cs new file mode 100644 index 00000000..f1622f56 --- /dev/null +++ b/WzComparerR2.Common/Encoders/FFmpegEncoder.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.IO; +using System.IO.Pipes; + +namespace WzComparerR2.Encoders +{ + public class FFmpegEncoder : GifEncoder + { + public static readonly string DefaultExecutionFileName = "ffmpeg"; + /// + /// Default ffmpeg argument format to encode mp4 with avc H.264 video format. + /// + public static readonly string DefaultArgumentFormat = @$"-y -f rawvideo -pixel_format bgra -s %w*%h -r 1000/%t -i ""%i"" -vf ""crop=trunc(iw/2)*2:trunc(ih/2)*2"" -vcodec libx264 -pix_fmt yuv420p ""%o"""; + + public static readonly string DefaultOutputFileExtension = ".mp4"; + + public FFmpegEncoder() + { + } + + public string FFmpegBinPath { get; set; } + public string FFmpegArgumentFormat { get; set; } + public string OutputFileExtension { get; set; } + + private Process ffmpegProc; + private NamedPipeServerStream server; + private CancellationTokenSource ffmpegProcExitedCts; + private StringBuilder ffmpegStdout = new StringBuilder(); + private StringBuilder ffmpegStderr = new StringBuilder(); + private bool disposed; + + public override GifEncoderCompatibility Compatibility => new GifEncoderCompatibility() + { + IsFixedFrameRate = true, + MinFrameDelay = 1, + MaxFrameDelay = int.MaxValue, + FrameDelayStep = 1, + AlphaSupportMode = AlphaSupportMode.NoAlpha, + DefaultExtension = string.IsNullOrEmpty(OutputFileExtension) ? DefaultOutputFileExtension : OutputFileExtension, + SupportedExtensions = new[] { ".*" }, + }; + + public unsafe override void AppendFrame(IntPtr pBuffer, int delay) + { + if (server == null && ffmpegProc == null) + { + StartFFmpeg(delay).Wait(); + } + + int frameDataLen = Width * Height * 4; + using UnmanagedMemoryStream ms = new((byte*)pBuffer.ToPointer(), frameDataLen, frameDataLen, FileAccess.Read); + ms.CopyToAsync(server, 32768, ffmpegProcExitedCts?.Token ?? default).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + private async Task StartFFmpeg(int delay) + { + // create random pipe name + string pipeName = $"{nameof(FFmpegEncoder)}-{Process.GetCurrentProcess().Id}-{(uint)Environment.TickCount}"; + + // create named-pipe server and listen + NamedPipeServerStream server = new(pipeName, PipeDirection.Out, 1, PipeTransmissionMode.Byte); + var task1 = server.WaitForConnectionAsync(); + + // start ffmpeg + ProcessStartInfo psi = new() + { + FileName = string.IsNullOrEmpty(FFmpegBinPath) ? DefaultExecutionFileName : FFmpegBinPath, + Arguments = SubstituteParams(string.IsNullOrEmpty(FFmpegArgumentFormat) ? DefaultArgumentFormat : FFmpegArgumentFormat, + @$"\\.\pipe\{pipeName}", Width, Height, delay, FileName), + WorkingDirectory = Environment.CurrentDirectory, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }; + Process ffmpegProc = new() + { + StartInfo = psi, + }; + ffmpegProc.OutputDataReceived += FFmpegProc_OutputDataReceived; + ffmpegProc.ErrorDataReceived += FFmpegProc_ErrorDataReceived; + ffmpegProc.Exited += FFmpegProc_Exited; + ffmpegProc.Start(); + ffmpegProc.BeginOutputReadLine(); + ffmpegProc.BeginErrorReadLine(); + await task1.ConfigureAwait(false); + + this.server = server; + this.ffmpegProc = ffmpegProc; + ffmpegProcExitedCts = new CancellationTokenSource(); + } + + private void FFmpegProc_OutputDataReceived(object sender, DataReceivedEventArgs e) + { + ffmpegStdout?.AppendLine(e.Data); + } + + private void FFmpegProc_ErrorDataReceived(object sender, DataReceivedEventArgs e) + { + ffmpegStderr?.AppendLine(e.Data); + } + + private void FFmpegProc_Exited(object sender, EventArgs e) + { + ffmpegProcExitedCts?.Cancel(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!disposed) + { + if (disposing) + { + if (server != null) + { + if (server.IsConnected) + { + server.Flush(); + server.Disconnect(); + } + server.Dispose(); + } + if (ffmpegProc != null) + { + if (!ffmpegProc.HasExited) + { + ffmpegProc.WaitForExit(); + } + ffmpegProc.Dispose(); + } + if (ffmpegProcExitedCts != null) + { + ffmpegProcExitedCts.Dispose(); + } + } + + ffmpegStdout = null; + ffmpegStderr = null; + disposed = true; + } + } + + private string SubstituteParams(string format, string inputFileName, int width, int height, int frameDelay, string outputFileName) + { + // %i: inputFileName + // %w: width + // %h: height + // %t: frameDelay + // %o: outputFileName + // %%: escape '%' char + return Regex.Replace(format, "%[iwhto%]", match => match.Value switch + { + "%i" => inputFileName, + "%w" => width.ToString(), + "%h" => height.ToString(), + "%t" => frameDelay.ToString(), + "%o" => outputFileName, + "%%" => "%", + _ => throw new FormatException($"Unknown format: {match.Value}") + }); + } + } +} diff --git a/WzComparerR2.Common/GifEncoder.cs b/WzComparerR2.Common/Encoders/GifEncoder.cs similarity index 62% rename from WzComparerR2.Common/GifEncoder.cs rename to WzComparerR2.Common/Encoders/GifEncoder.cs index ef5f460d..8014eb8b 100644 --- a/WzComparerR2.Common/GifEncoder.cs +++ b/WzComparerR2.Common/Encoders/GifEncoder.cs @@ -5,34 +5,46 @@ using System.Drawing; using System.Drawing.Imaging; -namespace WzComparerR2.Common +namespace WzComparerR2.Encoders { public abstract class GifEncoder : IDisposable { - protected GifEncoder(string fileName, int width, int height) + protected GifEncoder() { - this.FileName = fileName; - this.Width = width; - this.Height = height; } public string FileName { get; private set; } public int Width { get; private set; } public int Height { get; private set; } + public virtual string Name + { + get + { + return GetType().Name; + } + } + public abstract GifEncoderCompatibility Compatibility { get; } + + public virtual void Init(string fileName, int width, int height) + { + FileName = fileName; + Width = width; + Height = height; + } + public virtual void AppendFrame(Bitmap image, int delay) { BitmapData data = image.LockBits(new Rectangle(Point.Empty, image.Size), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); - this.AppendFrame(data.Scan0, delay); + AppendFrame(data.Scan0, delay); image.UnlockBits(data); } public abstract void AppendFrame(IntPtr pBuffer, int delay); - public void Dispose() { - this.Dispose(true); + Dispose(true); GC.SuppressFinalize(this); } @@ -43,7 +55,7 @@ protected virtual void Dispose(bool disposing) ~GifEncoder() { - this.Dispose(false); + Dispose(false); } } } diff --git a/WzComparerR2.Common/Encoders/GifEncoderCompatibility.cs b/WzComparerR2.Common/Encoders/GifEncoderCompatibility.cs new file mode 100644 index 00000000..c9019ff4 --- /dev/null +++ b/WzComparerR2.Common/Encoders/GifEncoderCompatibility.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WzComparerR2.Encoders +{ + public class GifEncoderCompatibility + { + public GifEncoderCompatibility() + { + } + + public bool IsFixedFrameRate { get; set; } + public int MinFrameDelay { get; set; } + public int MaxFrameDelay { get; set; } + public int FrameDelayStep { get; set; } + public AlphaSupportMode AlphaSupportMode { get; set; } + public string DefaultExtension { get; set; } + public IReadOnlyList SupportedExtensions { get; set; } + } + + public enum AlphaSupportMode + { + NoAlpha, + OneBitAlpha, + FullAlpha + } +} diff --git a/WzComparerR2.Common/IndexGifEncoder.cs b/WzComparerR2.Common/Encoders/IndexGifEncoder.cs similarity index 61% rename from WzComparerR2.Common/IndexGifEncoder.cs rename to WzComparerR2.Common/Encoders/IndexGifEncoder.cs index 33825bb5..f70be0ca 100644 --- a/WzComparerR2.Common/IndexGifEncoder.cs +++ b/WzComparerR2.Common/Encoders/IndexGifEncoder.cs @@ -3,35 +3,48 @@ using System.Drawing.Imaging; using System.Runtime.InteropServices; -namespace WzComparerR2.Common +namespace WzComparerR2.Encoders { public class IndexGifEncoder : GifEncoder { - public IndexGifEncoder(string location, int width, int height) - : this(location, width, height, 255, Color.FromArgb(0)) + public IndexGifEncoder() { - - } - public IndexGifEncoder(string location, int width, int height, int max_color, Color back_color) - :base(location, width, height) - { - encoder_pointer = construct(location, width, height, max_color, back_color.ToArgb()); } private IntPtr encoder_pointer; + public override GifEncoderCompatibility Compatibility => new GifEncoderCompatibility() + { + IsFixedFrameRate = false, + MinFrameDelay = 10, + MaxFrameDelay = 655350, + FrameDelayStep = 10, + AlphaSupportMode = AlphaSupportMode.OneBitAlpha, + DefaultExtension = ".gif", + SupportedExtensions = new[] { ".gif" }, + }; + + public override void Init(string fileName, int width, int height) + { + base.Init(fileName, width, height); + + encoder_pointer = construct(fileName, width, height, 255, 0); + } public override void AppendFrame(IntPtr pBuffer, int delay) { encoder.append_frame(pBuffer, delay, encoder_pointer); } - protected override void Dispose(bool disposing) { if (disposing) { - encoder.destruct(encoder_pointer); + if (encoder_pointer != IntPtr.Zero) + { + encoder.destruct(encoder_pointer); + encoder_pointer = IntPtr.Zero; + } } } diff --git a/WzComparerR2.Common/Gif.cs b/WzComparerR2.Common/Gif.cs index 6a7f51ac..3c5cfcf6 100644 --- a/WzComparerR2.Common/Gif.cs +++ b/WzComparerR2.Common/Gif.cs @@ -1,12 +1,9 @@ using System; using System.Collections.Generic; -using System.Text; -using System.IO; -using System.Runtime.InteropServices; using System.Drawing; using System.Drawing.Imaging; -using ImageManipulation; - +using System.IO; +using WzComparerR2.Encoders; using WzComparerR2.WzLib; namespace WzComparerR2.Common diff --git a/WzComparerR2/AnimateEncoderFactory.cs b/WzComparerR2/AnimateEncoderFactory.cs index 5a72b538..7be50884 100644 --- a/WzComparerR2/AnimateEncoderFactory.cs +++ b/WzComparerR2/AnimateEncoderFactory.cs @@ -1,90 +1,108 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using WzComparerR2.Common; using WzComparerR2.Config; +using WzComparerR2.Encoders; namespace WzComparerR2 { - public class AnimateEncoderFactory + public static class AnimateEncoderFactory { - private AnimateEncoderFactory() + static AnimateEncoderFactory() { - + registeredEncoders = new Dictionary(); + RegisterEncoders(); } - public static AnimateEncoderParams GetEncoderParams(int encoderID) + private static Dictionary registeredEncoders; + + private static void RegisterEncoders() { - switch (encoderID) + registeredEncoders.Add(0, new AnimateEncoderProvider + { + ID = 0, + Name = nameof(BuildInGifEncoder), + CreateEncoderCallback = () => new BuildInGifEncoder(), + }); + + registeredEncoders.Add(1, new AnimateEncoderProvider + { + ID = 1, + Name = nameof(IndexGifEncoder), + CreateEncoderCallback = () => new IndexGifEncoder(), + }); + + registeredEncoders.Add(2, new AnimateEncoderProvider { - default: - case 0: - return new AnimateEncoderParams() - { - ID = 0, - EncoderType = typeof(BuildInGifEncoder), - FileExtension = ".gif", - FileDescription = "Gif图片", - SupportAlphaChannel = false, - }; + ID = 2, + Name = nameof(BuildInApngEncoder), + CreateEncoderCallback = () => new BuildInApngEncoder(), + ConfigureEncoderCallback = (encoder, config) => + { + encoder.OptimizeEnabled = config.PaletteOptimized; + } + }); - case 1: - return new AnimateEncoderParams() - { - ID = 1, - EncoderType = typeof(BuildInGifEncoder), - FileExtension = ".gif", - FileDescription = "Gif图片", - SupportAlphaChannel = false, - }; + registeredEncoders.Add(3, new AnimateEncoderProvider + { + ID = 3, + Name = nameof(FFmpegEncoder), + CreateEncoderCallback = () => new FFmpegEncoder(), + ConfigureEncoderCallback = (encoder, config) => + { + encoder.FFmpegBinPath = config.FFmpegBinPath; + encoder.FFmpegArgumentFormat = config.FFmpegArgument; + encoder.OutputFileExtension = config.FFmpegOutputFileExtension; + } + }); + } + + public static GifEncoder CreateEncoder(ImageHandlerConfig config) + { + return CreateEncoder(config.GifEncoder, config); + } - case 2: - return new AnimateEncoderParams() - { - ID = 2, - EncoderType = typeof(BuildInGifEncoder), - FileExtension = ".png", - FileDescription = "APng图片", - SupportAlphaChannel = true, - }; + public static GifEncoder CreateEncoder(int id, ImageHandlerConfig config) + { + if (!registeredEncoders.TryGetValue(id, out var provider)) + { + throw new Exception($"Encoder ID {id} has not registered"); } + + var encoder = provider.CreateEncoder(); + provider.ConfigureEncoder(encoder, config); + return encoder; + } + + public interface IAnimateEncoderProvider + { + GifEncoder CreateEncoder(); + void ConfigureEncoder(GifEncoder encoder, ImageHandlerConfig config); } - public static GifEncoder CreateEncoder(string fileName, int width, int height, ImageHandlerConfig config) + public class AnimateEncoderProvider : IAnimateEncoderProvider where T : GifEncoder { - switch (config.GifEncoder.Value) + public int ID { get; set; } + public string Name { get; set; } + public Func CreateEncoderCallback { get; set; } + public Action ConfigureEncoderCallback { get; set; } + + public GifEncoder CreateEncoder() { - default: - case 0: - { - var enc = new BuildInGifEncoder(fileName, width, height); - return enc; - } + if (this.CreateEncoderCallback == null) + { + throw new ArgumentNullException(nameof(CreateEncoderCallback)); + } - case 1: - { - var enc = new IndexGifEncoder(fileName, width, height); - return enc; - } + return this.CreateEncoderCallback(); + } - case 2: - { - var enc = new BuildInApngEncoder(fileName, width, height); - enc.OptimizeEnabled = config.PaletteOptimized; - return enc; - } + public void ConfigureEncoder(GifEncoder encoder, ImageHandlerConfig config) + { + if (this.ConfigureEncoderCallback != null) + { + this.ConfigureEncoderCallback((T)encoder, config); + } } } } - - public class AnimateEncoderParams - { - public int ID { get; set; } - public Type EncoderType { get; set; } - public string FileExtension { get; set; } - public string FileDescription { get; set; } - - public bool SupportAlphaChannel { get; set; } - } } diff --git a/WzComparerR2/Config/ImageHandlerConfig.cs b/WzComparerR2/Config/ImageHandlerConfig.cs index 66a653ec..73eb919a 100644 --- a/WzComparerR2/Config/ImageHandlerConfig.cs +++ b/WzComparerR2/Config/ImageHandlerConfig.cs @@ -94,6 +94,27 @@ public ConfigItem PaletteOptimized get { return (ConfigItem)this["paletteOptimized"]; } set { this["paletteOptimized"] = value; } } + + [ConfigurationProperty("ffmpegBinPath")] + public ConfigItem FFmpegBinPath + { + get { return (ConfigItem)this["ffmpegBinPath"]; } + set { this["ffmpegBinPath"] = value; } + } + + [ConfigurationProperty("ffmpegArgument")] + public ConfigItem FFmpegArgument + { + get { return (ConfigItem)this["ffmpegArgument"]; } + set { this["ffmpegArgument"] = value; } + } + + [ConfigurationProperty("ffmpegOutputFileExtension")] + public ConfigItem FFmpegOutputFileExtension + { + get { return (ConfigItem)this["ffmpegOutputFileExtension"]; } + set { this["ffmpegOutputFileExtension"] = value; } + } } public enum ImageBackgroundType diff --git a/WzComparerR2/FrmGifSetting.Designer.cs b/WzComparerR2/FrmGifSetting.Designer.cs index afb4b90d..10dfcb43 100644 --- a/WzComparerR2/FrmGifSetting.Designer.cs +++ b/WzComparerR2/FrmGifSetting.Designer.cs @@ -32,13 +32,12 @@ private void InitializeComponent() this.colorPickerButton1 = new DevComponents.DotNetBar.ColorPickerButton(); this.labelX1 = new DevComponents.DotNetBar.LabelX(); this.checkBoxX1 = new DevComponents.DotNetBar.Controls.CheckBoxX(); - this.buttonX1 = new DevComponents.DotNetBar.ButtonX(); - this.buttonX2 = new DevComponents.DotNetBar.ButtonX(); this.labelX2 = new DevComponents.DotNetBar.LabelX(); this.comboBoxEx1 = new DevComponents.DotNetBar.Controls.ComboBoxEx(); this.comboItem1 = new DevComponents.Editors.ComboItem(); this.comboItem2 = new DevComponents.Editors.ComboItem(); this.comboItem6 = new DevComponents.Editors.ComboItem(); + this.comboItem7 = new DevComponents.Editors.ComboItem(); this.labelX3 = new DevComponents.DotNetBar.LabelX(); this.slider1 = new DevComponents.DotNetBar.Controls.Slider(); this.labelX4 = new DevComponents.DotNetBar.LabelX(); @@ -61,10 +60,34 @@ private void InitializeComponent() this.comboItem3 = new DevComponents.Editors.ComboItem(); this.comboItem4 = new DevComponents.Editors.ComboItem(); this.comboItem5 = new DevComponents.Editors.ComboItem(); + this.superTabControl1 = new DevComponents.DotNetBar.SuperTabControl(); + this.superTabControlPanel1 = new DevComponents.DotNetBar.SuperTabControlPanel(); + this.superTabItem1 = new DevComponents.DotNetBar.SuperTabItem(); + this.superTabControlPanel3 = new DevComponents.DotNetBar.SuperTabControlPanel(); + this.buttonX3 = new DevComponents.DotNetBar.ButtonX(); + this.textBoxX2 = new DevComponents.DotNetBar.Controls.TextBoxX(); + this.labelX11 = new DevComponents.DotNetBar.LabelX(); + this.textBoxX1 = new DevComponents.DotNetBar.Controls.TextBoxX(); + this.labelX10 = new DevComponents.DotNetBar.LabelX(); + this.superTabItem3 = new DevComponents.DotNetBar.SuperTabItem(); + this.superTabControlPanel2 = new DevComponents.DotNetBar.SuperTabControlPanel(); this.checkBoxX3 = new DevComponents.DotNetBar.Controls.CheckBoxX(); + this.superTabItem2 = new DevComponents.DotNetBar.SuperTabItem(); + this.panelEx1 = new DevComponents.DotNetBar.PanelEx(); + this.buttonX2 = new DevComponents.DotNetBar.ButtonX(); + this.buttonX1 = new DevComponents.DotNetBar.ButtonX(); + this.labelX12 = new DevComponents.DotNetBar.LabelX(); + this.textBoxX3 = new DevComponents.DotNetBar.Controls.TextBoxX(); + this.labelX13 = new DevComponents.DotNetBar.LabelX(); this.panelExMosaic.SuspendLayout(); this.panelExColor.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.integerInput1)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.superTabControl1)).BeginInit(); + this.superTabControl1.SuspendLayout(); + this.superTabControlPanel1.SuspendLayout(); + this.superTabControlPanel3.SuspendLayout(); + this.superTabControlPanel2.SuspendLayout(); + this.panelEx1.SuspendLayout(); this.SuspendLayout(); // // colorPickerButton1 @@ -107,39 +130,15 @@ private void InitializeComponent() this.checkBoxX1.TabIndex = 2; this.checkBoxX1.Text = "Background &Transparent"; // - // buttonX1 - // - this.buttonX1.AccessibleRole = System.Windows.Forms.AccessibleRole.PushButton; - this.buttonX1.ColorTable = DevComponents.DotNetBar.eButtonColor.OrangeWithBackground; - this.buttonX1.Location = new System.Drawing.Point(93, 276); - this.buttonX1.Name = "buttonX1"; - this.buttonX1.Size = new System.Drawing.Size(75, 23); - this.buttonX1.Style = DevComponents.DotNetBar.eDotNetBarStyle.StyleManagerControlled; - this.buttonX1.TabIndex = 10; - this.buttonX1.Text = "&OK"; - this.buttonX1.Click += new System.EventHandler(this.buttonX1_Click); - // - // buttonX2 - // - this.buttonX2.AccessibleRole = System.Windows.Forms.AccessibleRole.PushButton; - this.buttonX2.ColorTable = DevComponents.DotNetBar.eButtonColor.OrangeWithBackground; - this.buttonX2.DialogResult = System.Windows.Forms.DialogResult.Cancel; - this.buttonX2.Location = new System.Drawing.Point(189, 276); - this.buttonX2.Name = "buttonX2"; - this.buttonX2.Size = new System.Drawing.Size(75, 23); - this.buttonX2.Style = DevComponents.DotNetBar.eDotNetBarStyle.StyleManagerControlled; - this.buttonX2.TabIndex = 11; - this.buttonX2.Text = "&Cancel"; - this.buttonX2.Click += new System.EventHandler(this.buttonX2_Click); - // // labelX2 // this.labelX2.AutoSize = true; + this.labelX2.BackColor = System.Drawing.Color.Transparent; // // // this.labelX2.BackgroundStyle.CornerType = DevComponents.DotNetBar.eCornerType.Square; - this.labelX2.Location = new System.Drawing.Point(12, 199); + this.labelX2.Location = new System.Drawing.Point(16, 207); this.labelX2.Name = "labelX2"; this.labelX2.Size = new System.Drawing.Size(56, 16); this.labelX2.TabIndex = 5; @@ -155,8 +154,9 @@ private void InitializeComponent() this.comboBoxEx1.Items.AddRange(new object[] { this.comboItem1, this.comboItem2, - this.comboItem6}); - this.comboBoxEx1.Location = new System.Drawing.Point(76, 170); + this.comboItem6, + this.comboItem7}); + this.comboBoxEx1.Location = new System.Drawing.Point(80, 178); this.comboBoxEx1.Name = "comboBoxEx1"; this.comboBoxEx1.Size = new System.Drawing.Size(129, 21); this.comboBoxEx1.Style = DevComponents.DotNetBar.eDotNetBarStyle.StyleManagerControlled; @@ -174,6 +174,10 @@ private void InitializeComponent() // this.comboItem6.Text = "Apng Encoder"; // + // comboItem7 + // + this.comboItem7.Text = "FFmpeg Encoder"; + // // labelX3 // this.labelX3.AutoSize = true; @@ -208,11 +212,12 @@ private void InitializeComponent() // labelX4 // this.labelX4.AutoSize = true; + this.labelX4.BackColor = System.Drawing.Color.Transparent; // // // this.labelX4.BackgroundStyle.CornerType = DevComponents.DotNetBar.eCornerType.Square; - this.labelX4.Location = new System.Drawing.Point(12, 12); + this.labelX4.Location = new System.Drawing.Point(16, 20); this.labelX4.Name = "labelX4"; this.labelX4.Size = new System.Drawing.Size(68, 16); this.labelX4.TabIndex = 0; @@ -221,12 +226,13 @@ private void InitializeComponent() // rdoMosaic // this.rdoMosaic.AutoSize = true; + this.rdoMosaic.BackColor = System.Drawing.Color.Transparent; // // // this.rdoMosaic.BackgroundStyle.CornerType = DevComponents.DotNetBar.eCornerType.Square; this.rdoMosaic.CheckBoxStyle = DevComponents.DotNetBar.eCheckBoxStyle.RadioButton; - this.rdoMosaic.Location = new System.Drawing.Point(12, 122); + this.rdoMosaic.Location = new System.Drawing.Point(16, 130); this.rdoMosaic.Name = "rdoMosaic"; this.rdoMosaic.Size = new System.Drawing.Size(64, 16); this.rdoMosaic.Style = DevComponents.DotNetBar.eDotNetBarStyle.StyleManagerControlled; @@ -247,7 +253,7 @@ private void InitializeComponent() this.panelExMosaic.Controls.Add(this.colorPickerButton2); this.panelExMosaic.DisabledBackColor = System.Drawing.Color.Empty; this.panelExMosaic.Enabled = false; - this.panelExMosaic.Location = new System.Drawing.Point(76, 97); + this.panelExMosaic.Location = new System.Drawing.Point(80, 105); this.panelExMosaic.Name = "panelExMosaic"; this.panelExMosaic.Size = new System.Drawing.Size(256, 66); this.panelExMosaic.Style.Alignment = System.Drawing.StringAlignment.Center; @@ -354,7 +360,7 @@ private void InitializeComponent() this.panelExColor.Controls.Add(this.slider1); this.panelExColor.DisabledBackColor = System.Drawing.Color.Empty; this.panelExColor.Enabled = false; - this.panelExColor.Location = new System.Drawing.Point(76, 6); + this.panelExColor.Location = new System.Drawing.Point(80, 14); this.panelExColor.Name = "panelExColor"; this.panelExColor.Size = new System.Drawing.Size(256, 86); this.panelExColor.Style.Alignment = System.Drawing.StringAlignment.Center; @@ -369,12 +375,13 @@ private void InitializeComponent() // rdoColor // this.rdoColor.AutoSize = true; + this.rdoColor.BackColor = System.Drawing.Color.Transparent; // // // this.rdoColor.BackgroundStyle.CornerType = DevComponents.DotNetBar.eCornerType.Square; this.rdoColor.CheckBoxStyle = DevComponents.DotNetBar.eCheckBoxStyle.RadioButton; - this.rdoColor.Location = new System.Drawing.Point(12, 51); + this.rdoColor.Location = new System.Drawing.Point(16, 59); this.rdoColor.Name = "rdoColor"; this.rdoColor.Size = new System.Drawing.Size(57, 16); this.rdoColor.Style = DevComponents.DotNetBar.eDotNetBarStyle.StyleManagerControlled; @@ -390,11 +397,12 @@ private void InitializeComponent() // checkBoxX2 // this.checkBoxX2.AutoSize = true; + this.checkBoxX2.BackColor = System.Drawing.Color.Transparent; // // // this.checkBoxX2.BackgroundStyle.CornerType = DevComponents.DotNetBar.eCornerType.Square; - this.checkBoxX2.Location = new System.Drawing.Point(12, 223); + this.checkBoxX2.Location = new System.Drawing.Point(16, 231); this.checkBoxX2.Name = "checkBoxX2"; this.checkBoxX2.Size = new System.Drawing.Size(144, 16); this.checkBoxX2.Style = DevComponents.DotNetBar.eDotNetBarStyle.StyleManagerControlled; @@ -404,11 +412,12 @@ private void InitializeComponent() // labelX8 // this.labelX8.AutoSize = true; + this.labelX8.BackColor = System.Drawing.Color.Transparent; // // // this.labelX8.BackgroundStyle.CornerType = DevComponents.DotNetBar.eCornerType.Square; - this.labelX8.Location = new System.Drawing.Point(12, 248); + this.labelX8.Location = new System.Drawing.Point(16, 256); this.labelX8.Name = "labelX8"; this.labelX8.Size = new System.Drawing.Size(37, 16); this.labelX8.TabIndex = 8; @@ -423,7 +432,7 @@ private void InitializeComponent() this.integerInput1.BackgroundStyle.CornerType = DevComponents.DotNetBar.eCornerType.Square; this.integerInput1.ButtonFreeText.Shortcut = DevComponents.DotNetBar.eShortcut.F2; this.integerInput1.Increment = 10; - this.integerInput1.Location = new System.Drawing.Point(76, 245); + this.integerInput1.Location = new System.Drawing.Point(80, 253); this.integerInput1.MaxValue = 1000; this.integerInput1.MinValue = 10; this.integerInput1.Name = "integerInput1"; @@ -435,11 +444,12 @@ private void InitializeComponent() // labelX9 // this.labelX9.AutoSize = true; + this.labelX9.BackColor = System.Drawing.Color.Transparent; // // // this.labelX9.BackgroundStyle.CornerType = DevComponents.DotNetBar.eCornerType.Square; - this.labelX9.Location = new System.Drawing.Point(12, 174); + this.labelX9.Location = new System.Drawing.Point(16, 182); this.labelX9.Name = "labelX9"; this.labelX9.Size = new System.Drawing.Size(50, 16); this.labelX9.TabIndex = 16; @@ -456,7 +466,7 @@ private void InitializeComponent() this.comboItem3, this.comboItem4, this.comboItem5}); - this.comboBoxEx2.Location = new System.Drawing.Point(76, 197); + this.comboBoxEx2.Location = new System.Drawing.Point(80, 205); this.comboBoxEx2.Name = "comboBoxEx2"; this.comboBoxEx2.Size = new System.Drawing.Size(129, 21); this.comboBoxEx2.Style = DevComponents.DotNetBar.eDotNetBarStyle.StyleManagerControlled; @@ -474,39 +484,297 @@ private void InitializeComponent() // this.comboItem5.Text = "PathToWz"; // + // superTabControl1 + // + // + // + // + // + // + // + this.superTabControl1.ControlBox.CloseBox.Name = ""; + // + // + // + this.superTabControl1.ControlBox.MenuBox.Name = ""; + this.superTabControl1.ControlBox.Name = ""; + this.superTabControl1.ControlBox.SubItems.AddRange(new DevComponents.DotNetBar.BaseItem[] { + this.superTabControl1.ControlBox.MenuBox, + this.superTabControl1.ControlBox.CloseBox}); + this.superTabControl1.Controls.Add(this.superTabControlPanel3); + this.superTabControl1.Controls.Add(this.superTabControlPanel1); + this.superTabControl1.Controls.Add(this.superTabControlPanel2); + this.superTabControl1.Dock = System.Windows.Forms.DockStyle.Fill; + this.superTabControl1.Location = new System.Drawing.Point(0, 0); + this.superTabControl1.Name = "superTabControl1"; + this.superTabControl1.ReorderTabsEnabled = true; + this.superTabControl1.SelectedTabFont = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold); + this.superTabControl1.SelectedTabIndex = 0; + this.superTabControl1.Size = new System.Drawing.Size(454, 311); + this.superTabControl1.TabAlignment = DevComponents.DotNetBar.eTabStripAlignment.Left; + this.superTabControl1.TabFont = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.superTabControl1.TabIndex = 30; + this.superTabControl1.Tabs.AddRange(new DevComponents.DotNetBar.BaseItem[] { + this.superTabItem1, + this.superTabItem2, + this.superTabItem3}); + this.superTabControl1.Text = "superTabControl1"; + // + // superTabControlPanel1 + // + this.superTabControlPanel1.Controls.Add(this.comboBoxEx2); + this.superTabControlPanel1.Controls.Add(this.labelX9); + this.superTabControlPanel1.Controls.Add(this.labelX2); + this.superTabControlPanel1.Controls.Add(this.integerInput1); + this.superTabControlPanel1.Controls.Add(this.comboBoxEx1); + this.superTabControlPanel1.Controls.Add(this.labelX8); + this.superTabControlPanel1.Controls.Add(this.rdoMosaic); + this.superTabControlPanel1.Controls.Add(this.checkBoxX2); + this.superTabControlPanel1.Controls.Add(this.panelExColor); + this.superTabControlPanel1.Controls.Add(this.rdoColor); + this.superTabControlPanel1.Controls.Add(this.panelExMosaic); + this.superTabControlPanel1.Controls.Add(this.labelX4); + this.superTabControlPanel1.Dock = System.Windows.Forms.DockStyle.Fill; + this.superTabControlPanel1.Location = new System.Drawing.Point(105, 0); + this.superTabControlPanel1.Name = "superTabControlPanel1"; + this.superTabControlPanel1.Size = new System.Drawing.Size(349, 311); + this.superTabControlPanel1.TabIndex = 1; + this.superTabControlPanel1.TabItem = this.superTabItem1; + // + // superTabItem1 + // + this.superTabItem1.AttachedControl = this.superTabControlPanel1; + this.superTabItem1.GlobalItem = false; + this.superTabItem1.Name = "superTabItem1"; + this.superTabItem1.Text = "General"; + // + // superTabControlPanel3 + // + this.superTabControlPanel3.Controls.Add(this.labelX13); + this.superTabControlPanel3.Controls.Add(this.textBoxX3); + this.superTabControlPanel3.Controls.Add(this.labelX12); + this.superTabControlPanel3.Controls.Add(this.buttonX3); + this.superTabControlPanel3.Controls.Add(this.textBoxX2); + this.superTabControlPanel3.Controls.Add(this.labelX11); + this.superTabControlPanel3.Controls.Add(this.textBoxX1); + this.superTabControlPanel3.Controls.Add(this.labelX10); + this.superTabControlPanel3.Dock = System.Windows.Forms.DockStyle.Fill; + this.superTabControlPanel3.Location = new System.Drawing.Point(108, 0); + this.superTabControlPanel3.Name = "superTabControlPanel3"; + this.superTabControlPanel3.Size = new System.Drawing.Size(346, 311); + this.superTabControlPanel3.TabIndex = 0; + this.superTabControlPanel3.TabItem = this.superTabItem3; + // + // buttonX3 + // + this.buttonX3.AccessibleRole = System.Windows.Forms.AccessibleRole.PushButton; + this.buttonX3.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.buttonX3.ColorTable = DevComponents.DotNetBar.eButtonColor.OrangeWithBackground; + this.buttonX3.Location = new System.Drawing.Point(315, 14); + this.buttonX3.Name = "buttonX3"; + this.buttonX3.Size = new System.Drawing.Size(24, 24); + this.buttonX3.Style = DevComponents.DotNetBar.eDotNetBarStyle.StyleManagerControlled; + this.buttonX3.Symbol = ""; + this.buttonX3.SymbolSize = 12F; + this.buttonX3.TabIndex = 15; + this.buttonX3.Click += new System.EventHandler(this.buttonX3_Click); + // + // textBoxX2 + // + this.textBoxX2.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + // + // + // + this.textBoxX2.Border.Class = "TextBoxBorder"; + this.textBoxX2.Border.CornerType = DevComponents.DotNetBar.eCornerType.Square; + this.textBoxX2.Location = new System.Drawing.Point(109, 46); + this.textBoxX2.Multiline = true; + this.textBoxX2.Name = "textBoxX2"; + this.textBoxX2.PreventEnterBeep = true; + this.textBoxX2.ScrollBars = System.Windows.Forms.ScrollBars.Vertical; + this.textBoxX2.Size = new System.Drawing.Size(201, 124); + this.textBoxX2.TabIndex = 14; + // + // labelX11 + // + this.labelX11.AutoSize = true; + this.labelX11.BackColor = System.Drawing.Color.Transparent; + // + // + // + this.labelX11.BackgroundStyle.CornerType = DevComponents.DotNetBar.eCornerType.Square; + this.labelX11.Location = new System.Drawing.Point(16, 50); + this.labelX11.Name = "labelX11"; + this.labelX11.Size = new System.Drawing.Size(93, 16); + this.labelX11.TabIndex = 13; + this.labelX11.Text = "FFmpegArgument"; + // + // textBoxX1 + // + this.textBoxX1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + // + // + // + this.textBoxX1.Border.Class = "TextBoxBorder"; + this.textBoxX1.Border.CornerType = DevComponents.DotNetBar.eCornerType.Square; + this.textBoxX1.Location = new System.Drawing.Point(109, 16); + this.textBoxX1.Name = "textBoxX1"; + this.textBoxX1.PreventEnterBeep = true; + this.textBoxX1.Size = new System.Drawing.Size(201, 21); + this.textBoxX1.TabIndex = 12; + // + // labelX10 + // + this.labelX10.AutoSize = true; + this.labelX10.BackColor = System.Drawing.Color.Transparent; + // + // + // + this.labelX10.BackgroundStyle.CornerType = DevComponents.DotNetBar.eCornerType.Square; + this.labelX10.Location = new System.Drawing.Point(16, 20); + this.labelX10.Name = "labelX10"; + this.labelX10.Size = new System.Drawing.Size(87, 16); + this.labelX10.TabIndex = 10; + this.labelX10.Text = "FFmpegBinPath"; + // + // superTabItem3 + // + this.superTabItem3.AttachedControl = this.superTabControlPanel3; + this.superTabItem3.GlobalItem = false; + this.superTabItem3.Name = "superTabItem3"; + this.superTabItem3.Text = "FFmpegEncoder"; + // + // superTabControlPanel2 + // + this.superTabControlPanel2.Controls.Add(this.checkBoxX3); + this.superTabControlPanel2.Dock = System.Windows.Forms.DockStyle.Fill; + this.superTabControlPanel2.Location = new System.Drawing.Point(105, 0); + this.superTabControlPanel2.Name = "superTabControlPanel2"; + this.superTabControlPanel2.Size = new System.Drawing.Size(349, 291); + this.superTabControlPanel2.TabIndex = 0; + this.superTabControlPanel2.TabItem = this.superTabItem2; + // // checkBoxX3 // this.checkBoxX3.AutoSize = true; + this.checkBoxX3.BackColor = System.Drawing.Color.Transparent; // // // this.checkBoxX3.BackgroundStyle.CornerType = DevComponents.DotNetBar.eCornerType.Square; - this.checkBoxX3.Location = new System.Drawing.Point(212, 174); + this.checkBoxX3.Location = new System.Drawing.Point(16, 20); this.checkBoxX3.Name = "checkBoxX3"; - this.checkBoxX3.Size = new System.Drawing.Size(76, 16); + this.checkBoxX3.Size = new System.Drawing.Size(255, 16); this.checkBoxX3.Style = DevComponents.DotNetBar.eDotNetBarStyle.StyleManagerControlled; - this.checkBoxX3.TabIndex = 23; - this.checkBoxX3.Text = "Optimize"; + this.checkBoxX3.TabIndex = 24; + this.checkBoxX3.Text = "Optimize File Size (8bit palette png)"; + // + // superTabItem2 + // + this.superTabItem2.AttachedControl = this.superTabControlPanel2; + this.superTabItem2.GlobalItem = false; + this.superTabItem2.Name = "superTabItem2"; + this.superTabItem2.Text = "APngEncoder"; + // + // panelEx1 + // + this.panelEx1.CanvasColor = System.Drawing.SystemColors.Control; + this.panelEx1.ColorSchemeStyle = DevComponents.DotNetBar.eDotNetBarStyle.StyleManagerControlled; + this.panelEx1.Controls.Add(this.buttonX2); + this.panelEx1.Controls.Add(this.buttonX1); + this.panelEx1.DisabledBackColor = System.Drawing.Color.Empty; + this.panelEx1.Dock = System.Windows.Forms.DockStyle.Bottom; + this.panelEx1.Location = new System.Drawing.Point(0, 311); + this.panelEx1.Name = "panelEx1"; + this.panelEx1.Size = new System.Drawing.Size(454, 30); + this.panelEx1.Style.Alignment = System.Drawing.StringAlignment.Center; + this.panelEx1.Style.BackColor1.ColorSchemePart = DevComponents.DotNetBar.eColorSchemePart.PanelBackground; + this.panelEx1.Style.BackColor2.ColorSchemePart = DevComponents.DotNetBar.eColorSchemePart.PanelBackground2; + this.panelEx1.Style.Border = DevComponents.DotNetBar.eBorderType.SingleLine; + this.panelEx1.Style.BorderColor.ColorSchemePart = DevComponents.DotNetBar.eColorSchemePart.PanelBorder; + this.panelEx1.Style.ForeColor.ColorSchemePart = DevComponents.DotNetBar.eColorSchemePart.PanelText; + this.panelEx1.Style.GradientAngle = 90; + this.panelEx1.TabIndex = 38; + // + // buttonX2 + // + this.buttonX2.AccessibleRole = System.Windows.Forms.AccessibleRole.PushButton; + this.buttonX2.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.buttonX2.ColorTable = DevComponents.DotNetBar.eButtonColor.OrangeWithBackground; + this.buttonX2.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.buttonX2.Location = new System.Drawing.Point(385, 4); + this.buttonX2.Name = "buttonX2"; + this.buttonX2.Size = new System.Drawing.Size(60, 23); + this.buttonX2.Style = DevComponents.DotNetBar.eDotNetBarStyle.StyleManagerControlled; + this.buttonX2.TabIndex = 1; + this.buttonX2.Text = "取消"; + // + // buttonX1 + // + this.buttonX1.AccessibleRole = System.Windows.Forms.AccessibleRole.PushButton; + this.buttonX1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.buttonX1.ColorTable = DevComponents.DotNetBar.eButtonColor.OrangeWithBackground; + this.buttonX1.DialogResult = System.Windows.Forms.DialogResult.OK; + this.buttonX1.Location = new System.Drawing.Point(318, 4); + this.buttonX1.Name = "buttonX1"; + this.buttonX1.Size = new System.Drawing.Size(60, 23); + this.buttonX1.Style = DevComponents.DotNetBar.eDotNetBarStyle.StyleManagerControlled; + this.buttonX1.TabIndex = 0; + this.buttonX1.Text = "确定"; + // + // labelX12 + // + this.labelX12.AutoSize = true; + this.labelX12.BackColor = System.Drawing.Color.Transparent; + // + // + // + this.labelX12.BackgroundStyle.CornerType = DevComponents.DotNetBar.eCornerType.Square; + this.labelX12.Location = new System.Drawing.Point(16, 184); + this.labelX12.Name = "labelX12"; + this.labelX12.Size = new System.Drawing.Size(87, 16); + this.labelX12.TabIndex = 16; + this.labelX12.Text = "OutputFileExt"; + // + // textBoxX3 + // + this.textBoxX3.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + // + // + // + this.textBoxX3.Border.Class = "TextBoxBorder"; + this.textBoxX3.Border.CornerType = DevComponents.DotNetBar.eCornerType.Square; + this.textBoxX3.Location = new System.Drawing.Point(109, 180); + this.textBoxX3.Name = "textBoxX3"; + this.textBoxX3.PreventEnterBeep = true; + this.textBoxX3.Size = new System.Drawing.Size(75, 21); + this.textBoxX3.TabIndex = 17; + // + // labelX13 + // + this.labelX13.AutoSize = true; + this.labelX13.BackColor = System.Drawing.Color.Transparent; + // + // + // + this.labelX13.BackgroundStyle.CornerType = DevComponents.DotNetBar.eCornerType.Square; + this.labelX13.ForeColor = System.Drawing.SystemColors.ControlDarkDark; + this.labelX13.Location = new System.Drawing.Point(16, 207); + this.labelX13.Name = "labelX13"; + this.labelX13.Size = new System.Drawing.Size(126, 86); + this.labelX13.TabIndex = 18; + this.labelX13.Text = "参数通配符:
\r\n  %i 输入文件名
\r\n  %o 输出文件名
\r\n  %w 输入图像宽度(px)
\r\n  %h 输出图像高度(px)
\r\n  %t 帧间隔(ms)"; // // FrmGifSetting // this.CancelButton = this.buttonX2; - this.ClientSize = new System.Drawing.Size(344, 311); - this.Controls.Add(this.checkBoxX3); - this.Controls.Add(this.comboBoxEx2); - this.Controls.Add(this.labelX9); - this.Controls.Add(this.integerInput1); - this.Controls.Add(this.labelX8); - this.Controls.Add(this.checkBoxX2); - this.Controls.Add(this.rdoColor); - this.Controls.Add(this.panelExColor); - this.Controls.Add(this.panelExMosaic); - this.Controls.Add(this.rdoMosaic); - this.Controls.Add(this.labelX4); - this.Controls.Add(this.comboBoxEx1); - this.Controls.Add(this.labelX2); - this.Controls.Add(this.buttonX2); - this.Controls.Add(this.buttonX1); + this.ClientSize = new System.Drawing.Size(454, 341); + this.Controls.Add(this.superTabControl1); + this.Controls.Add(this.panelEx1); this.DoubleBuffered = true; this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; this.MaximizeBox = false; @@ -519,8 +787,16 @@ private void InitializeComponent() this.panelExColor.ResumeLayout(false); this.panelExColor.PerformLayout(); ((System.ComponentModel.ISupportInitialize)(this.integerInput1)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.superTabControl1)).EndInit(); + this.superTabControl1.ResumeLayout(false); + this.superTabControlPanel1.ResumeLayout(false); + this.superTabControlPanel1.PerformLayout(); + this.superTabControlPanel3.ResumeLayout(false); + this.superTabControlPanel3.PerformLayout(); + this.superTabControlPanel2.ResumeLayout(false); + this.superTabControlPanel2.PerformLayout(); + this.panelEx1.ResumeLayout(false); this.ResumeLayout(false); - this.PerformLayout(); } @@ -529,8 +805,6 @@ private void InitializeComponent() private DevComponents.DotNetBar.ColorPickerButton colorPickerButton1; private DevComponents.DotNetBar.LabelX labelX1; private DevComponents.DotNetBar.Controls.CheckBoxX checkBoxX1; - private DevComponents.DotNetBar.ButtonX buttonX1; - private DevComponents.DotNetBar.ButtonX buttonX2; private DevComponents.DotNetBar.LabelX labelX2; private DevComponents.DotNetBar.Controls.ComboBoxEx comboBoxEx1; private DevComponents.Editors.ComboItem comboItem1; @@ -558,6 +832,25 @@ private void InitializeComponent() private DevComponents.Editors.ComboItem comboItem4; private DevComponents.Editors.ComboItem comboItem5; private DevComponents.Editors.ComboItem comboItem6; + private DevComponents.DotNetBar.SuperTabControl superTabControl1; + private DevComponents.DotNetBar.SuperTabControlPanel superTabControlPanel1; + private DevComponents.DotNetBar.SuperTabItem superTabItem1; + private DevComponents.DotNetBar.PanelEx panelEx1; + private DevComponents.DotNetBar.ButtonX buttonX2; + private DevComponents.DotNetBar.ButtonX buttonX1; + private DevComponents.DotNetBar.SuperTabControlPanel superTabControlPanel2; + private DevComponents.DotNetBar.SuperTabItem superTabItem2; + private DevComponents.DotNetBar.SuperTabControlPanel superTabControlPanel3; + private DevComponents.DotNetBar.SuperTabItem superTabItem3; private DevComponents.DotNetBar.Controls.CheckBoxX checkBoxX3; + private DevComponents.DotNetBar.LabelX labelX10; + private DevComponents.DotNetBar.ButtonX buttonX3; + private DevComponents.DotNetBar.Controls.TextBoxX textBoxX2; + private DevComponents.DotNetBar.LabelX labelX11; + private DevComponents.DotNetBar.Controls.TextBoxX textBoxX1; + private DevComponents.Editors.ComboItem comboItem7; + private DevComponents.DotNetBar.LabelX labelX12; + private DevComponents.DotNetBar.Controls.TextBoxX textBoxX3; + private DevComponents.DotNetBar.LabelX labelX13; } } \ No newline at end of file diff --git a/WzComparerR2/FrmGifSetting.cs b/WzComparerR2/FrmGifSetting.cs index c9965573..d48fa052 100644 --- a/WzComparerR2/FrmGifSetting.cs +++ b/WzComparerR2/FrmGifSetting.cs @@ -130,6 +130,39 @@ public bool PaletteOptimized set { checkBoxX3.Checked = value; } } + public string FFmpegBinPath + { + get { return textBoxX1.Text; } + set { textBoxX1.Text = value; } + } + + public string FFmpegArgument + { + get { return textBoxX2.Text; } + set { textBoxX2.Text = value; } + } + + public string FFmpegDefaultExtension + { + get { return textBoxX3.Text; } + set { textBoxX3.Text = value; } + } + + public string FFmpegBinPathHint + { + set { textBoxX1.WatermarkText = value; } + } + + public string FFmpegArgumentHint + { + set { textBoxX2.WatermarkText = value; } + } + + public string FFmpegDefaultExtensionHint + { + set { textBoxX3.WatermarkText = value; } + } + public void Load(ImageHandlerConfig config) { this.SavePngFramesEnabled = config.SavePngFramesEnabled; @@ -145,6 +178,10 @@ public void Load(ImageHandlerConfig config) this.MosaicBlockSize = config.MosaicInfo.BlockSize; this.PaletteOptimized = config.PaletteOptimized; + + this.FFmpegBinPath = config.FFmpegBinPath; + this.FFmpegArgument = config.FFmpegArgument; + this.FFmpegDefaultExtension = config.FFmpegOutputFileExtension; } public void Save(ImageHandlerConfig config) @@ -162,18 +199,10 @@ public void Save(ImageHandlerConfig config) config.MosaicInfo.BlockSize = this.MosaicBlockSize; config.PaletteOptimized = this.PaletteOptimized; - } - - private void buttonX1_Click(object sender, EventArgs e) - { - this.DialogResult = DialogResult.OK; - this.Close(); - } - private void buttonX2_Click(object sender, EventArgs e) - { - this.DialogResult = DialogResult.Cancel; - this.Close(); + config.FFmpegBinPath = this.FFmpegBinPath; + config.FFmpegArgument = this.FFmpegArgument; + config.FFmpegOutputFileExtension = this.FFmpegDefaultExtension; } private void slider1_ValueChanged(object sender, EventArgs e) @@ -192,5 +221,16 @@ private void rdoMosaic_CheckedChanged(object sender, EventArgs e) panelExMosaic.Enabled = rdoMosaic.Checked; } + private void buttonX3_Click(object sender, EventArgs e) + { + OpenFileDialog dlg = new(); + dlg.Title = "请选择FFmpeg可执行文件路径..."; + dlg.Filter = "ffmpeg.exe|*.exe|*.*|*.*"; + dlg.FileName = this.FFmpegBinPath; + if (dlg.ShowDialog(this) == DialogResult.OK) + { + this.FFmpegBinPath = dlg.FileName; + } + } } } \ No newline at end of file diff --git a/WzComparerR2/MainForm.cs b/WzComparerR2/MainForm.cs index 0103b044..c4e29305 100644 --- a/WzComparerR2/MainForm.cs +++ b/WzComparerR2/MainForm.cs @@ -1,32 +1,33 @@ -using System; -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel; using System.Data; -using System.Drawing; using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Linq; using System.Text; using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; using System.Windows.Forms; -using System.Linq; -using System.IO; using System.Xml; +using static Microsoft.Xna.Framework.MathHelper; using Timer = System.Timers.Timer; -using System.Threading; -using System.Threading.Tasks; +using DevComponents.AdvTree; using DevComponents.DotNetBar; using DevComponents.DotNetBar.Controls; -using DevComponents.AdvTree; -using WzComparerR2.WzLib; -using WzComparerR2.Common; -using WzComparerR2.CharaSimControl; -using WzComparerR2.PluginBase; + +using WzComparerR2.Animation; using WzComparerR2.CharaSim; +using WzComparerR2.CharaSimControl; +using WzComparerR2.Common; using WzComparerR2.Comparer; -using WzComparerR2.Controls; -using WzComparerR2.Rendering; using WzComparerR2.Config; -using WzComparerR2.Animation; -using static Microsoft.Xna.Framework.MathHelper; +using WzComparerR2.Controls; +using WzComparerR2.Encoders; +using WzComparerR2.PluginBase; +using WzComparerR2.WzLib; namespace WzComparerR2 { @@ -510,6 +511,9 @@ private void buttonItemGifSetting_Click(object sender, EventArgs e) { FrmGifSetting frm = new FrmGifSetting(); frm.Load(ImageHandlerConfig.Default); + frm.FFmpegBinPathHint = FFmpegEncoder.DefaultExecutionFileName; + frm.FFmpegArgumentHint = FFmpegEncoder.DefaultArgumentFormat; + frm.FFmpegDefaultExtensionHint = FFmpegEncoder.DefaultOutputFileExtension; if (frm.ShowDialog() == DialogResult.OK) { ConfigManager.Reload(); @@ -602,12 +606,13 @@ private void OnSavePngFile(Frame frame) private void OnSaveGifFile(AnimationItem aniItem, bool options) { var config = ImageHandlerConfig.Default; - var encParams = AnimateEncoderFactory.GetEncoderParams(config.GifEncoder.Value); + using var encoder = AnimateEncoderFactory.CreateEncoder(config); + var cap = encoder.Compatibility; string aniName = this.cmbItemAniNames.SelectedItem as string; string aniFileName = pictureBoxEx1.PictureName + (string.IsNullOrEmpty(aniName) ? "" : ("." + aniName)) - + encParams.FileExtension; + + cap.DefaultExtension; if (config.AutoSaveEnabled) { @@ -624,8 +629,8 @@ private void OnSaveGifFile(AnimationItem aniItem, bool options) else { var dlg = new SaveFileDialog(); - - dlg.Filter = string.Format("{0}(*{1})|*{1}|全部文件(*.*)|*.*", encParams.FileDescription, encParams.FileExtension); + string extensionFilter = string.Join(";", cap.SupportedExtensions.Select(ext => $"*{ext}")); + dlg.Filter = string.Format("{0} Supported Files ({1})|{1}|All files (*.*)|*.*", encoder.Name, extensionFilter); dlg.FileName = aniFileName; if (dlg.ShowDialog() != DialogResult.OK) @@ -636,7 +641,7 @@ private void OnSaveGifFile(AnimationItem aniItem, bool options) } var clonedAniItem = (AnimationItem)aniItem.Clone(); - if (this.pictureBoxEx1.SaveAsGif(clonedAniItem, aniFileName, config, options)) + if (this.pictureBoxEx1.SaveAsGif(clonedAniItem, aniFileName, config, encoder, options)) { labelItemStatus.Text = "图片保存于" + aniFileName; } diff --git a/WzComparerR2/PictureBoxEx.cs b/WzComparerR2/PictureBoxEx.cs index 95743e70..3497fff2 100644 --- a/WzComparerR2/PictureBoxEx.cs +++ b/WzComparerR2/PictureBoxEx.cs @@ -1,20 +1,21 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using System.IO; +using System.Linq; using System.Runtime.InteropServices; +using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using Microsoft.Xna.Framework; -using WzComparerR2.WzLib; -using WzComparerR2.Controls; using WzComparerR2.Animation; -using WzComparerR2.Rendering; -using WzComparerR2.Config; using WzComparerR2.Common; +using WzComparerR2.Config; +using WzComparerR2.Controls; +using WzComparerR2.Encoders; +using WzComparerR2.Rendering; +using WzComparerR2.WzLib; namespace WzComparerR2 { @@ -144,14 +145,19 @@ public void AdjustPosition() } } - public bool SaveAsGif(AnimationItem aniItem, string fileName, ImageHandlerConfig config, bool options) + public bool SaveAsGif(AnimationItem aniItem, string fileName, ImageHandlerConfig config, GifEncoder encoder, bool showOptions) { var rec = new AnimationRecoder(this.GraphicsDevice); + var cap = encoder.Compatibility; rec.Items.Add(aniItem); int length = rec.GetMaxLength(); - int delay = Math.Max(10, config.MinDelay); - var timeline = rec.GetGifTimeLine(delay, 655350); + int delay = Math.Max(cap.MinFrameDelay, config.MinDelay); + int[] timeline = null; + if (!cap.IsFixedFrameRate) + { + timeline = rec.GetGifTimeLine(delay, cap.MaxFrameDelay); + } // calc available canvas area rec.ResetAll(); @@ -183,7 +189,7 @@ public bool SaveAsGif(AnimationItem aniItem, string fileName, ImageHandlerConfig OutputHeight = bounds.Height, }; - if (options) + if (showOptions) { var frmOptions = new FrmGifClipOptions() { @@ -259,8 +265,7 @@ public bool SaveAsGif(AnimationItem aniItem, string fileName, ImageHandlerConfig } // select encoder - GifEncoder enc = AnimateEncoderFactory.CreateEncoder(fileName, targetSize.X, targetSize.Y, config); - var encParams = AnimateEncoderFactory.GetEncoderParams(config.GifEncoder.Value); + encoder.Init(fileName, targetSize.X, targetSize.Y); // pipeline functions IEnumerable> MergeFrames(IEnumerable> frames) @@ -278,7 +283,7 @@ IEnumerable> MergeFrames(IEnumerable> fram prevFrame = currentFrame; prevDelay = currentDelay; } - else if (memcmp(prevFrame, currentFrame, (IntPtr)prevFrame.Length) == 0) + else if (prevFrame.AsSpan().SequenceEqual(currentFrame.AsSpan())) { prevDelay += currentDelay; } @@ -345,7 +350,7 @@ IEnumerable ClipTimeline(int[] _timeline) async Task ApplyFrame(byte[] frameData, int frameDelay) { byte[] gifData = null; - if (!encParams.SupportAlphaChannel && config.BackgroundType.Value == ImageBackgroundType.Transparent) + if (cap.AlphaSupportMode != AlphaSupportMode.FullAlpha && config.BackgroundType.Value == ImageBackgroundType.Transparent) { using (var rt2 = rec.GetGifTexture(config.BackgroundColor.Value.ToXnaColor(), config.MinMixedAlpha)) { @@ -369,16 +374,18 @@ async Task ApplyFrame(byte[] frameData, int frameDelay) tasks.Add(Task.Run(() => { string pngFileName = Path.Combine(framesDirName, $"{prevTime}_{prevTime + frameDelay}.png"); - unsafe + GCHandle gcHandle = GCHandle.Alloc(frameData, GCHandleType.Pinned); + try { - fixed (byte* pFrameBuffer = frameData) + using (var bmp = new System.Drawing.Bitmap(targetSize.X, targetSize.Y, targetSize.X * 4, System.Drawing.Imaging.PixelFormat.Format32bppArgb, gcHandle.AddrOfPinnedObject())) { - using (var bmp = new System.Drawing.Bitmap(targetSize.X, targetSize.Y, targetSize.X * 4, System.Drawing.Imaging.PixelFormat.Format32bppArgb, new IntPtr(pFrameBuffer))) - { - bmp.Save(pngFileName, System.Drawing.Imaging.ImageFormat.Png); - } + bmp.Save(pngFileName, System.Drawing.Imaging.ImageFormat.Png); } } + finally + { + gcHandle.Free(); + } })); } @@ -387,12 +394,15 @@ async Task ApplyFrame(byte[] frameData, int frameDelay) { // TODO: only for gif here? frameDelay = Math.Max(10, (int)(Math.Round(frameDelay / 10.0) * 10)); - unsafe + + GCHandle gcHandle = GCHandle.Alloc(frameData, GCHandleType.Pinned); + try { - fixed (byte* pGifBuffer = gifData) - { - enc.AppendFrame(new IntPtr(pGifBuffer), frameDelay); - } + encoder.AppendFrame(gcHandle.AddrOfPinnedObject(), frameDelay); + } + finally + { + gcHandle.Free(); } })); @@ -403,7 +413,7 @@ async Task ApplyFrame(byte[] frameData, int frameDelay) async Task RenderJob(IProgressDialogContext context, CancellationToken cancellationToken) { - bool isCompareAndMergeFrames = timeline == null; + bool isCompareAndMergeFrames = timeline == null && !cap.IsFixedFrameRate; // build pipeline IEnumerable delayEnumerator = timeline == null ? RenderDelay() : ClipTimeline(timeline); @@ -459,7 +469,6 @@ async Task RenderJob(IProgressDialogContext context, CancellationToken cancellat finally { rec.End(); - enc.Dispose(); this.IsPlaying = isPlaying; } } @@ -543,10 +552,5 @@ private void UpdateInfoText() aniItem.Length); } } - - - [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)] - private static extern int memcmp(byte[] b1, byte[] b2, IntPtr count); - } } From 70b88576d040b09f04f82ac79be65e44f064d17c Mon Sep 17 00:00:00 2001 From: Kagamia Date: Sun, 5 Jan 2025 13:48:29 +0800 Subject: [PATCH 2/7] Common: optimize error message displaying for progress dialog. --- .../Controls/FrmProgressDialog.Designer.cs | 1 + WzComparerR2.Common/Controls/FrmProgressDialog.cs | 12 ++++++++++++ .../Controls/ProgressDialogContext.cs | 7 +++++++ WzComparerR2/PictureBoxEx.cs | 10 +++++++++- 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/WzComparerR2.Common/Controls/FrmProgressDialog.Designer.cs b/WzComparerR2.Common/Controls/FrmProgressDialog.Designer.cs index 33b7dc02..d2abfdf4 100644 --- a/WzComparerR2.Common/Controls/FrmProgressDialog.Designer.cs +++ b/WzComparerR2.Common/Controls/FrmProgressDialog.Designer.cs @@ -65,6 +65,7 @@ private void InitializeComponent() this.labelX1.Size = new System.Drawing.Size(278, 19); this.labelX1.TabIndex = 0; this.labelX1.Text = "Message"; + this.labelX1.MouseClick += new System.Windows.Forms.MouseEventHandler(this.labelX1_MouseClick); // // progressBarX1 // diff --git a/WzComparerR2.Common/Controls/FrmProgressDialog.cs b/WzComparerR2.Common/Controls/FrmProgressDialog.cs index c40e3b30..b2b8e127 100644 --- a/WzComparerR2.Common/Controls/FrmProgressDialog.cs +++ b/WzComparerR2.Common/Controls/FrmProgressDialog.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using System.Windows.Forms; +using DevComponents.DotNetBar; namespace WzComparerR2.Controls { @@ -38,5 +39,16 @@ public int ProgressMax get { return this.progressBarX1.Maximum; } set { this.progressBarX1.Maximum = value; } } + + public string FullMessage { get; set; } + + private void labelX1_MouseClick(object sender, MouseEventArgs e) + { + if (e.Button == MouseButtons.Right) + { + Clipboard.SetText(this.FullMessage ?? this.Message); + ToastNotification.Show(this, "已复制到剪切板。", 1000, eToastPosition.TopCenter); + } + } } } diff --git a/WzComparerR2.Common/Controls/ProgressDialogContext.cs b/WzComparerR2.Common/Controls/ProgressDialogContext.cs index 7b3dcdf8..9c253816 100644 --- a/WzComparerR2.Common/Controls/ProgressDialogContext.cs +++ b/WzComparerR2.Common/Controls/ProgressDialogContext.cs @@ -11,6 +11,7 @@ namespace WzComparerR2.Controls public interface IProgressDialogContext { string Message { get; set; } + string FullMessage { get; set; } int Progress { get; set; } int ProgressMin { get; set; } int ProgressMax { get; set; } @@ -46,6 +47,12 @@ public string Message set { this.dialog.Message = value; } } + public string FullMessage + { + get { return this.dialog.FullMessage; } + set { this.dialog.FullMessage = value; } + } + public int Progress { get { return this.dialog.Progress; } diff --git a/WzComparerR2/PictureBoxEx.cs b/WzComparerR2/PictureBoxEx.cs index 3497fff2..9b409eac 100644 --- a/WzComparerR2/PictureBoxEx.cs +++ b/WzComparerR2/PictureBoxEx.cs @@ -463,7 +463,15 @@ async Task RenderJob(IProgressDialogContext context, CancellationToken cancellat } catch (Exception ex) { - context.Message = $"Error: {ex.Message}"; + if (ex is AggregateException aggrEx && aggrEx.InnerExceptions.Count == 1) + { + context.Message = $"Error: {aggrEx.InnerExceptions[0].Message}"; + } + else + { + context.Message = $"Error: {ex.Message}"; + } + context.FullMessage = ex.ToString(); throw; } finally From c82fbb7e3e3b0be1cf1dd3ce38165c6c1a29d75d Mon Sep 17 00:00:00 2001 From: seotbeo Date: Sun, 5 Jan 2025 18:11:27 +0900 Subject: [PATCH 3/7] Maprender: Fix NullReferenceException --- WzComparerR2.MapRender/UI/Tooltip2.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WzComparerR2.MapRender/UI/Tooltip2.cs b/WzComparerR2.MapRender/UI/Tooltip2.cs index 691071c6..eac828bf 100644 --- a/WzComparerR2.MapRender/UI/Tooltip2.cs +++ b/WzComparerR2.MapRender/UI/Tooltip2.cs @@ -400,8 +400,8 @@ private TooltipContent DrawItem(GameTime gameTime, RenderEnv env, UIWorldMap.Map this.StringLinker?.StringNpc.TryGetValue(npcID, out sr); string npcText = sr?.Name ?? npcID.ToString(); var npcInfo = PluginManager.FindWz(string.Format("Npc/{0:D7}.img/info", npcID)); - var hide = npcInfo.Nodes["hide"].GetValueEx(0); - var hideName = npcInfo.Nodes["hideName"].GetValueEx(0); + var hide = npcInfo?.Nodes["hide"].GetValueEx(0); + var hideName = npcInfo?.Nodes["hideName"].GetValueEx(0); if ((hide != 0 || hideName != 0) && npcNames.Contains(npcText)) { npcNames[npcNames.IndexOf(npcText)] = ""; From ccd4fc52a83ea7018511d95cb46db8ac48057854 Mon Sep 17 00:00:00 2001 From: Kagamia Date: Mon, 6 Jan 2025 07:19:07 +0800 Subject: [PATCH 4/7] CharaSim: support render item with multiple recipes (2512293) --- WzComparerR2.Common/CharaSim/Item.cs | 19 ++- .../CharaSimControl/ItemTooltipRender2.cs | 149 ++++++++++-------- 2 files changed, 99 insertions(+), 69 deletions(-) diff --git a/WzComparerR2.Common/CharaSim/Item.cs b/WzComparerR2.Common/CharaSim/Item.cs index 6b85ec22..ae8cc6cc 100644 --- a/WzComparerR2.Common/CharaSim/Item.cs +++ b/WzComparerR2.Common/CharaSim/Item.cs @@ -12,12 +12,14 @@ public Item() { this.Props = new Dictionary(); this.Specs = new Dictionary(); + this.Recipes = new List(); } public int Level { get; set; } public Dictionary Props { get; private set; } public Dictionary Specs { get; private set; } + public List Recipes { get; private set; } public bool Cash { @@ -109,8 +111,21 @@ public static Item CreateFromNode(Wz_Node node, GlobalFindNodeFunction findNode) { foreach (Wz_Node subNode in specNode.Nodes) { - ItemSpecType type; - if (Enum.TryParse(subNode.Text, out type)) + if (subNode.Text == "recipe") + { + if (subNode.Value == null && subNode.Nodes.Count > 0) + { + foreach (var recipeNode in subNode.Nodes) + { + item.Recipes.Add(recipeNode.GetValue()); + } + } + else + { + item.Recipes.Add(subNode.GetValue()); + } + } + else if(Enum.TryParse(subNode.Text, out ItemSpecType type)) { try { diff --git a/WzComparerR2/CharaSimControl/ItemTooltipRender2.cs b/WzComparerR2/CharaSimControl/ItemTooltipRender2.cs index fc098543..31cff4e2 100644 --- a/WzComparerR2/CharaSimControl/ItemTooltipRender2.cs +++ b/WzComparerR2/CharaSimControl/ItemTooltipRender2.cs @@ -58,73 +58,76 @@ public override Bitmap Render() //绘制道具 int picHeight; Bitmap itemBmp = RenderItem(out picHeight); - Bitmap recipeInfoBmp = null; - Bitmap recipeItemBmp = null; + List recipeInfoBmps = new(); + List recipeItemBmps = new(); Bitmap setItemBmp = null; //图纸相关 - if (this.item.Specs.TryGetValue(ItemSpecType.recipe, out long recipeID)) + if (this.item.Recipes.Count > 0) { - long recipeSkillID = recipeID / 10000; - Recipe recipe = null; - //寻找配方 - Wz_Node recipeNode = PluginBase.PluginManager.FindWz(string.Format(@"Skill\Recipe_{0}.img\{1}", recipeSkillID, recipeID)); - if (recipeNode != null) + foreach (int recipeID in this.item.Recipes) { - recipe = Recipe.CreateFromNode(recipeNode); - } - //生成配方图像 - if (recipe != null) - { - if (this.LinkRecipeInfo) + int recipeSkillID = recipeID / 10000; + Recipe recipe = null; + //寻找配方 + Wz_Node recipeNode = PluginBase.PluginManager.FindWz(string.Format(@"Skill\Recipe_{0}.img\{1}", recipeSkillID, recipeID)); + if (recipeNode != null) { - recipeInfoBmp = RenderLinkRecipeInfo(recipe); + recipe = Recipe.CreateFromNode(recipeNode); } - - if (this.LinkRecipeItem) + //生成配方图像 + if (recipe != null) { - int itemID = recipe.MainTargetItemID; - int itemIDClass = itemID / 1000000; - if (itemIDClass == 1) //通过ID寻找装备 + if (this.LinkRecipeInfo) { - Wz_Node charaWz = PluginManager.FindWz(Wz_Type.Character); - if (charaWz != null) + recipeInfoBmps.Add(RenderLinkRecipeInfo(recipe)); + } + + if (this.LinkRecipeItem) + { + int itemID = recipe.MainTargetItemID; + int itemIDClass = itemID / 1000000; + if (itemIDClass == 1) //通过ID寻找装备 { - string imgName = itemID.ToString("d8")+".img"; - foreach (Wz_Node node0 in charaWz.Nodes) + Wz_Node charaWz = PluginManager.FindWz(Wz_Type.Character); + if (charaWz != null) { - Wz_Node imgNode = node0.FindNodeByPath(imgName, true); - if (imgNode != null) + string imgName = itemID.ToString("d8") + ".img"; + foreach (Wz_Node node0 in charaWz.Nodes) { - Gear gear = Gear.CreateFromNode(imgNode, path=>PluginManager.FindWz(path)); - if (gear != null) + Wz_Node imgNode = node0.FindNodeByPath(imgName, true); + if (imgNode != null) { - recipeItemBmp = RenderLinkRecipeGear(gear); - } + Gear gear = Gear.CreateFromNode(imgNode, path => PluginManager.FindWz(path)); + if (gear != null) + { + recipeItemBmps.Add(RenderLinkRecipeGear(gear)); + } - break; + break; + } } } } - } - else if (itemIDClass >= 2 && itemIDClass <= 5) //通过ID寻找道具 - { - Wz_Node itemWz = PluginManager.FindWz(Wz_Type.Item); - if (itemWz != null) + else if (itemIDClass >= 2 && itemIDClass <= 5) //通过ID寻找道具 { - string imgClass = (itemID / 10000).ToString("d4") + ".img\\"+itemID.ToString("d8"); - foreach (Wz_Node node0 in itemWz.Nodes) + Wz_Node itemWz = PluginManager.FindWz(Wz_Type.Item); + if (itemWz != null) { - Wz_Node imgNode = node0.FindNodeByPath(imgClass, true); - if (imgNode != null) + string imgClass = (itemID / 10000).ToString("d4") + ".img\\" + itemID.ToString("d8"); + foreach (Wz_Node node0 in itemWz.Nodes) { - Item item = Item.CreateFromNode(imgNode, PluginManager.FindWz); - if (item != null) + Wz_Node imgNode = node0.FindNodeByPath(imgClass, true); + if (imgNode != null) { - recipeItemBmp = RenderLinkRecipeItem(item); - } + Item item = Item.CreateFromNode(imgNode, PluginManager.FindWz); + if (item != null) + { + recipeItemBmps.Add(RenderLinkRecipeItem(item)); + } - break; + break; + } } } } @@ -148,26 +151,31 @@ public override Bitmap Render() Point recipeItemOrigin = Point.Empty; Point setItemOrigin = Point.Empty; - if (recipeItemBmp != null) + if (recipeItemBmps.Count > 0) { + // layout: + // item | recipeItem + // recipeInfo | recipeItemOrigin.X = totalSize.Width; - totalSize.Width += recipeItemBmp.Width; + totalSize.Width += recipeItemBmps.Max(bmp => bmp.Width); - if (recipeInfoBmp != null) + if (recipeInfoBmps.Count > 0) { - recipeInfoOrigin.X = itemBmp.Width - recipeInfoBmp.Width; + recipeInfoOrigin.X = itemBmp.Width - recipeInfoBmps.Max(bmp => bmp.Width); recipeInfoOrigin.Y = picHeight; - totalSize.Height = Math.Max(picHeight + recipeInfoBmp.Height, recipeItemBmp.Height); + totalSize.Height = Math.Max(picHeight + recipeInfoBmps.Sum(bmp => bmp.Height), recipeItemBmps.Sum(bmp => bmp.Height)); } else { - totalSize.Height = Math.Max(picHeight, recipeItemBmp.Height); + totalSize.Height = Math.Max(picHeight, recipeItemBmps.Sum(bmp => bmp.Height)); } } - else if (recipeInfoBmp != null) + else if (recipeInfoBmps.Count > 0) { - totalSize.Width += recipeInfoBmp.Width; - totalSize.Height = Math.Max(picHeight, recipeInfoBmp.Height); + // layout: + // item | recipeInfo + totalSize.Width += recipeInfoBmps.Max(bmp => bmp.Width); + totalSize.Height = Math.Max(picHeight, recipeInfoBmps.Sum(bmp => bmp.Height)); recipeInfoOrigin.X = itemBmp.Width; } if (setItemBmp != null) @@ -197,17 +205,25 @@ public override Bitmap Render() } //绘制配方 - if (recipeInfoBmp != null) + if (recipeInfoBmps.Count > 0) { - g.DrawImage(recipeInfoBmp, recipeInfoOrigin.X, recipeInfoOrigin.Y, - new Rectangle(Point.Empty, recipeInfoBmp.Size), GraphicsUnit.Pixel); + for (int i = 0, y = recipeInfoOrigin.Y; i < recipeInfoBmps.Count; i++) + { + g.DrawImage(recipeInfoBmps[i], recipeInfoOrigin.X, y, + new Rectangle(Point.Empty, recipeInfoBmps[i].Size), GraphicsUnit.Pixel); + y += recipeInfoBmps[i].Height; + } } //绘制产出道具 - if (recipeItemBmp != null) + if (recipeItemBmps.Count > 0) { - g.DrawImage(recipeItemBmp, recipeItemOrigin.X, recipeItemOrigin.Y, - new Rectangle(Point.Empty, recipeItemBmp.Size), GraphicsUnit.Pixel); + for (int i = 0, y = recipeItemOrigin.Y; i < recipeItemBmps.Count; i++) + { + g.DrawImage(recipeItemBmps[i], recipeItemOrigin.X, y, + new Rectangle(Point.Empty, recipeItemBmps[i].Size), GraphicsUnit.Pixel); + y += recipeItemBmps[i].Height; + } } //绘制套装 @@ -219,10 +235,10 @@ public override Bitmap Render() if (itemBmp != null) itemBmp.Dispose(); - if (recipeInfoBmp != null) - recipeInfoBmp.Dispose(); - if (recipeItemBmp != null) - recipeItemBmp.Dispose(); + if (recipeInfoBmps.Count > 0) + recipeInfoBmps.ForEach(bmp => bmp.Dispose()); + if (recipeItemBmps.Count > 0) + recipeItemBmps.ForEach(bmp => bmp.Dispose()); if (setItemBmp != null) setItemBmp.Dispose(); @@ -230,7 +246,6 @@ public override Bitmap Render() return tooltip; } - private Bitmap RenderItem(out int picH) { StringFormat format = (StringFormat)StringFormat.GenericDefault.Clone(); @@ -442,12 +457,12 @@ private Bitmap RenderItem(out int picH) //绘制配方需求 - if (item.Specs.TryGetValue(ItemSpecType.recipe, out value)) + if (item.Recipes.Count > 0) { long reqSkill, reqSkillLevel; if (!item.Specs.TryGetValue(ItemSpecType.reqSkill, out reqSkill)) { - reqSkill = value / 10000 * 10000; + reqSkill = item.Recipes[0] / 10000 * 10000; } if (!item.Specs.TryGetValue(ItemSpecType.reqSkillLevel, out reqSkillLevel)) From 60e0daf82f0c18b800a4bb68693cd6867204219f Mon Sep 17 00:00:00 2001 From: seotbeo Date: Mon, 6 Jan 2025 17:15:21 +0900 Subject: [PATCH 5/7] CharaSim: Find DamageSkin Sample --- WzComparerR2.Common/CharaSim/Item.cs | 5 +++++ WzComparerR2/CharaSimControl/ItemTooltipRender2.cs | 13 ++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/WzComparerR2.Common/CharaSim/Item.cs b/WzComparerR2.Common/CharaSim/Item.cs index d14a2656..0f732874 100644 --- a/WzComparerR2.Common/CharaSim/Item.cs +++ b/WzComparerR2.Common/CharaSim/Item.cs @@ -19,6 +19,7 @@ public Item() } public int Level { get; set; } + public int? DamageSkinID { get; set; } public string ConsumableFrom { get; set; } public string EndUseDate { get; set; } public string SamplePath { get; set; } @@ -99,6 +100,10 @@ public static Item CreateFromNode(Wz_Node node, GlobalFindNodeFunction findNode) item.Level = Convert.ToInt32(subNode.Value); break; + case "damageSkinID": + item.DamageSkinID = Convert.ToInt32(subNode.Value); + break; + case "consumableFrom": item.ConsumableFrom = Convert.ToString(subNode.Value); break; diff --git a/WzComparerR2/CharaSimControl/ItemTooltipRender2.cs b/WzComparerR2/CharaSimControl/ItemTooltipRender2.cs index 6f397243..558d0d30 100644 --- a/WzComparerR2/CharaSimControl/ItemTooltipRender2.cs +++ b/WzComparerR2/CharaSimControl/ItemTooltipRender2.cs @@ -781,7 +781,7 @@ private Bitmap RenderItem(out int picH) long minLev = 0, maxLev = 0; bool willDrawExp = item.Props.TryGetValue(ItemPropType.exp_minLev, out minLev) && item.Props.TryGetValue(ItemPropType.exp_maxLev, out maxLev); - if (!string.IsNullOrEmpty(descLeftAlign) || item.CoreSpecs.Count > 0 || item.Sample.Bitmap != null || item.SamplePath != null || willDrawNickTag || willDrawExp) + if (!string.IsNullOrEmpty(descLeftAlign) || item.CoreSpecs.Count > 0 || item.Sample.Bitmap != null || item.DamageSkinID != null || item.SamplePath != null || willDrawNickTag || willDrawExp) { if (picH < iconY + 84) { @@ -838,6 +838,17 @@ private Bitmap RenderItem(out int picH) picH += item.Sample.Bitmap.Height; picH += 2; } + else if (item.DamageSkinID != null) + { + Wz_Node sampleNode = PluginManager.FindWz($@"Etc\DamageSkin.img\{item.DamageSkinID}\sample"); + if (sampleNode != null) + { + BitmapOrigin sample = BitmapOrigin.CreateFromNode(sampleNode, PluginManager.FindWz); + g.DrawImage(sample.Bitmap, (tooltip.Width - sample.Bitmap.Width) / 2, picH); + picH += sample.Bitmap.Height; + picH += 2; + } + } if (item.SamplePath != null) { Wz_Node sampleNode = PluginManager.FindWz(item.SamplePath); From 87d0e06a8059759f6758511ed00309962c5992f7 Mon Sep 17 00:00:00 2001 From: seotbeo Date: Tue, 7 Jan 2025 17:41:11 +0900 Subject: [PATCH 6/7] Comparer: Seperate _Canvas files to _Canvas directory --- WzComparerR2/Comparer/EasyComparer.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/WzComparerR2/Comparer/EasyComparer.cs b/WzComparerR2/Comparer/EasyComparer.cs index 34fba33f..f47fbb9d 100644 --- a/WzComparerR2/Comparer/EasyComparer.cs +++ b/WzComparerR2/Comparer/EasyComparer.cs @@ -836,6 +836,7 @@ protected virtual string OutputNodeValue(string fullPath, Wz_Node value, int col string colName = col == 0 ? "new" : (col == 1 ? "old" : col.ToString()); string fileName = fullPath.Replace('\\', '.'); string suffix = "_" + colName + ".png"; + string canvas = "_Canvas"; if (this.HashPngFileName) { @@ -858,11 +859,21 @@ protected virtual string OutputNodeValue(string fullPath, Wz_Node value, int col } fileName = fileName + suffix; + string outputDirName = new DirectoryInfo(outputDir).Name; + bool isCanvas = fileName.Contains(canvas); + if (isCanvas) + { + outputDir = Path.Combine(outputDir, canvas); + if (!Directory.Exists(outputDir)) + { + Directory.CreateDirectory(outputDir); + } + } using (Bitmap bmp = png.ExtractPng()) { bmp.Save(Path.Combine(outputDir, fileName), System.Drawing.Imaging.ImageFormat.Png); } - return string.Format("", new DirectoryInfo(outputDir).Name, WebUtility.UrlEncode(fileName)); + return string.Format("", isCanvas ? Path.Combine(outputDirName, canvas) : outputDirName, WebUtility.UrlEncode(fileName)); } else { From 33db1f15d2274bfa99952d6fdb4dbd6077e8ea92 Mon Sep 17 00:00:00 2001 From: seotbeo Date: Tue, 7 Jan 2025 01:28:08 +0900 Subject: [PATCH 7/7] Avatar: Fix hairShade Rendering --- WzComparerR2.Avatar/AvatarCanvas.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WzComparerR2.Avatar/AvatarCanvas.cs b/WzComparerR2.Avatar/AvatarCanvas.cs index 2a68de7a..8c094ef8 100644 --- a/WzComparerR2.Avatar/AvatarCanvas.cs +++ b/WzComparerR2.Avatar/AvatarCanvas.cs @@ -949,9 +949,9 @@ private void CreateBone(Bone root, Tuple[] frameNodes, bo { continue; } - if (childNode.Text == "hairShade") + if (linkNode.Text == "hairShade") { - linkNode = childNode.FindNodeByPath("0"); + linkNode = linkNode.FindNodeByPath("0"); if (linkNode == null) { continue;