public bool SaveAsGif(AnimationItem aniItem, string fileName, ImageHandlerConfig config, bool options) { var rec = new AnimationRecoder(this.GraphicsDevice); rec.Items.Add(aniItem); int length = rec.GetMaxLength(); int delay = Math.Max(10, config.MinDelay); var timeline = rec.GetGifTimeLine(delay, 655350); // calc available canvas area rec.ResetAll(); Microsoft.Xna.Framework.Rectangle bounds = aniItem.Measure(); if (length > 0) { IEnumerable <int> delays = timeline?.Take(timeline.Length - 1) ?? Enumerable.Range(0, (int)Math.Ceiling(1.0 * length / delay) - 1); foreach (var frameDelay in delays) { rec.Update(TimeSpan.FromMilliseconds(frameDelay)); var rect = aniItem.Measure(); bounds = Microsoft.Xna.Framework.Rectangle.Union(bounds, rect); } } bounds.Offset(aniItem.Position); // customize clip/scale options AnimationClipOptions clipOptions = new AnimationClipOptions() { StartTime = 0, StopTime = length, Left = bounds.Left, Top = bounds.Top, Right = bounds.Right, Bottom = bounds.Bottom, OutputWidth = bounds.Width, OutputHeight = bounds.Height, }; if (options) { var frmOptions = new FrmGifClipOptions() { ClipOptions = clipOptions, ClipOptionsNew = clipOptions, }; if (frmOptions.ShowDialog() == DialogResult.OK) { var clipOptionsNew = frmOptions.ClipOptionsNew; clipOptions.StartTime = clipOptionsNew.StartTime ?? clipOptions.StartTime; clipOptions.StopTime = clipOptionsNew.StopTime ?? clipOptions.StopTime; clipOptions.Left = clipOptionsNew.Left ?? clipOptions.Left; clipOptions.Top = clipOptionsNew.Top ?? clipOptions.Top; clipOptions.Right = clipOptionsNew.Right ?? clipOptions.Right; clipOptions.Bottom = clipOptionsNew.Bottom ?? clipOptions.Bottom; clipOptions.OutputWidth = clipOptionsNew.OutputWidth ?? (clipOptions.Right - clipOptions.Left); clipOptions.OutputHeight = clipOptionsNew.OutputHeight ?? (clipOptions.Bottom - clipOptions.Top); } else { return(false); } } // validate params bounds = new Rectangle( clipOptions.Left.Value, clipOptions.Top.Value, clipOptions.Right.Value - clipOptions.Left.Value, clipOptions.Bottom.Value - clipOptions.Top.Value ); var targetSize = new Point(clipOptions.OutputWidth.Value, clipOptions.OutputHeight.Value); var startTime = clipOptions.StartTime.Value; var stopTime = clipOptions.StopTime.Value; if (bounds.Width <= 0 || bounds.Height <= 0 || targetSize.X <= 0 || targetSize.Y <= 0 || startTime < 0 || stopTime > length || stopTime - startTime < 0) { return(false); } length = stopTime - startTime; // create output dir string framesDirName = Path.Combine(Path.GetDirectoryName(fileName), Path.GetFileNameWithoutExtension(fileName) + ".frames"); if (config.SavePngFramesEnabled && !Directory.Exists(framesDirName)) { Directory.CreateDirectory(framesDirName); } // pre-render rec.ResetAll(); switch (config.BackgroundType.Value) { default: case ImageBackgroundType.Transparent: rec.BackgroundColor = Color.TransparentBlack; break; case ImageBackgroundType.Color: rec.BackgroundColor = System.Drawing.Color.FromArgb(255, config.BackgroundColor.Value).ToXnaColor(); break; case ImageBackgroundType.Mosaic: rec.BackgroundImage = MonogameUtils.CreateMosaic(GraphicsDevice, config.MosaicInfo.Color0.ToXnaColor(), config.MosaicInfo.Color1.ToXnaColor(), Math.Max(1, config.MosaicInfo.BlockSize)); break; } // select encoder GifEncoder enc = AnimateEncoderFactory.CreateEncoder(fileName, targetSize.X, targetSize.Y, config); var encParams = AnimateEncoderFactory.GetEncoderParams(config.GifEncoder.Value); // pipeline functions IEnumerable <Tuple <byte[], int> > MergeFrames(IEnumerable <Tuple <byte[], int> > frames) { byte[] prevFrame = null; int prevDelay = 0; foreach (var frame in frames) { byte[] currentFrame = frame.Item1; int currentDelay = frame.Item2; if (prevFrame == null) { prevFrame = currentFrame; prevDelay = currentDelay; } else if (memcmp(prevFrame, currentFrame, (IntPtr)prevFrame.Length) == 0) { prevDelay += currentDelay; } else { yield return(Tuple.Create(prevFrame, prevDelay)); prevFrame = currentFrame; prevDelay = currentDelay; } } if (prevFrame != null) { yield return(Tuple.Create(prevFrame, prevDelay)); } } IEnumerable <int> RenderDelay() { int t = 0; while (t < length) { int frameDelay = Math.Min(length - t, delay); t += frameDelay; yield return(frameDelay); } } IEnumerable <int> ClipTimeline(int[] _timeline) { int t = 0; for (int i = 0; i < timeline.Length; i++) { var frameDelay = timeline[i]; if (t < startTime) { if (t + frameDelay > startTime) { frameDelay = t + frameDelay - startTime; t = startTime; } else { t += frameDelay; continue; } } if (t + frameDelay < stopTime) { yield return(frameDelay); t += frameDelay; } else { frameDelay = stopTime - t; yield return(frameDelay); break; } } } int prevTime = 0; async Task <int> ApplyFrame(byte[] frameData, int frameDelay) { byte[] gifData = null; if (!encParams.SupportAlphaChannel && config.BackgroundType.Value == ImageBackgroundType.Transparent) { using (var rt2 = rec.GetGifTexture(config.BackgroundColor.Value.ToXnaColor(), config.MinMixedAlpha)) { if (gifData == null) { gifData = new byte[frameData.Length]; } rt2.GetData(gifData); } } else { gifData = frameData; } var tasks = new List <Task>(); // save each frame as png if (config.SavePngFramesEnabled) { tasks.Add(Task.Run(() => { string pngFileName = Path.Combine(framesDirName, $"{prevTime}_{prevTime + frameDelay}.png"); unsafe { fixed(byte *pFrameBuffer = frameData) { 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); } } } })); } // append frame data to gif stream tasks.Add(Task.Run(() => { // TODO: only for gif here? frameDelay = Math.Max(10, (int)(Math.Round(frameDelay / 10.0) * 10)); unsafe { fixed(byte *pGifBuffer = gifData) { enc.AppendFrame(new IntPtr(pGifBuffer), frameDelay); } } })); await Task.WhenAll(tasks); prevTime += frameDelay; return(prevTime); } async Task RenderJob(IProgressDialogContext context, CancellationToken cancellationToken) { bool isCompareAndMergeFrames = timeline == null; // build pipeline IEnumerable <int> delayEnumerator = timeline == null?RenderDelay() : ClipTimeline(timeline); var step1 = delayEnumerator.TakeWhile(_ => !cancellationToken.IsCancellationRequested); var frameRenderEnumerator = step1.Select(frameDelay => { rec.Draw(); rec.Update(TimeSpan.FromMilliseconds(frameDelay)); return(frameDelay); }); var step2 = frameRenderEnumerator.TakeWhile(_ => !cancellationToken.IsCancellationRequested); var getFrameData = step2.Select(frameDelay => { using (var t2d = rec.GetPngTexture()) { byte[] frameData = new byte[t2d.Width * t2d.Height * 4]; t2d.GetData(frameData); return(Tuple.Create(frameData, frameDelay)); } }); var step3 = getFrameData.TakeWhile(_ => !cancellationToken.IsCancellationRequested); if (isCompareAndMergeFrames) { var mergedFrameData = MergeFrames(step3); step3 = mergedFrameData.TakeWhile(_ => !cancellationToken.IsCancellationRequested); } var step4 = step3.Select(item => ApplyFrame(item.Item1, item.Item2)); // run pipeline bool isPlaying = this.IsPlaying; try { this.IsPlaying = false; rec.Begin(bounds, targetSize); if (startTime > 0) { rec.Update(TimeSpan.FromMilliseconds(startTime)); } context.ProgressMin = 0; context.ProgressMax = length; foreach (var task in step4) { int currentTime = await task; context.Progress = currentTime; } } catch (Exception ex) { context.Message = $"Error: {ex.Message}"; throw; } finally { rec.End(); enc.Dispose(); this.IsPlaying = isPlaying; } } var dialogResult = ProgressDialog.Show(this.FindForm(), "Exporting...", "Save animation file...", true, false, RenderJob); return(dialogResult == DialogResult.OK); }
public void SaveAsGif(AnimationItem aniItem, string fileName, ImageHandlerConfig config) { //保存动画 天坑代码 var rec = new AnimationRecoder(this.GraphicsDevice); rec.Items.Add(aniItem); int length = rec.GetMaxLength(); //rec.GetGifTimeLine(30); 放弃获取时间轴 //预判大小阶段 rec.ResetAll(); Microsoft.Xna.Framework.Rectangle?bounds = null; int delay = Math.Max(10, config.MinDelay); for (int d = 0; d <= length; d += delay) { rec.Update(TimeSpan.FromMilliseconds(delay)); var rect = aniItem.Measure(); rect.Offset(aniItem.Position); bounds = (bounds == null || bounds.Value.IsEmpty) ? rect : Microsoft.Xna.Framework.Rectangle.Union(rect, bounds.Value); } //绘制阶段 rec.ResetAll(); switch (config.BackgroundType.Value) { default: case ImageBackgroundType.Transparent: rec.BackgroundColor = Color.TransparentBlack; break; case ImageBackgroundType.Color: rec.BackgroundColor = System.Drawing.Color.FromArgb(255, config.BackgroundColor.Value).ToXnaColor(); break; case ImageBackgroundType.Mosaic: rec.BackgroundImage = MonogameUtils.CreateMosaic(GraphicsDevice, config.MosaicInfo.Color0.ToXnaColor(), config.MosaicInfo.Color1.ToXnaColor(), Math.Max(1, config.MosaicInfo.BlockSize)); break; } byte[] buffer = new byte[bounds.Value.Width * bounds.Value.Height * 4]; byte[] bufferPrev = null, bufferGif = null; int gifTime = 0, prevTime = 0; string dirName = Path.Combine(Path.GetDirectoryName(fileName), Path.GetFileNameWithoutExtension(fileName) + ".frames"); if (config.SavePngFramesEnabled && !Directory.Exists(dirName)) { Directory.CreateDirectory(dirName); } //选择encoder GifEncoder enc = AnimateEncoderFactory.CreateEncoder(fileName, bounds.Value.Width, bounds.Value.Height, config); var encParams = AnimateEncoderFactory.GetEncoderParams(config.GifEncoder.Value); Action <int> writeFrame = (curTime) => { unsafe { string pngFileName = Path.Combine(dirName, prevTime + ".png"); fixed(byte *pBuffer = bufferPrev, pBufferGif = bufferGif) { using (var bmp = new System.Drawing.Bitmap(bounds.Value.Width, bounds.Value.Height, bounds.Value.Width * 4, System.Drawing.Imaging.PixelFormat.Format32bppArgb, new IntPtr(pBuffer))) { var tasks = new List <Task>(); if (config.SavePngFramesEnabled) //保存单帧图像 { tasks.Add(Task.Factory.StartNew( () => bmp.Save(pngFileName, System.Drawing.Imaging.ImageFormat.Png) )); } //保存gif帧 int frameDelay = curTime - gifTime; frameDelay = (int)(Math.Round(frameDelay / 10.0) * 10); if (frameDelay > 0) { gifTime += frameDelay; var pData = new IntPtr(pBufferGif); tasks.Add(Task.Factory.StartNew(() => enc.AppendFrame(pData, frameDelay) )); } Task.WaitAll(tasks.ToArray()); } } } }; rec.Begin(bounds.Value); //0长度补丁 for (int d = 0; d <= length; d += delay, rec.Update(TimeSpan.FromMilliseconds(delay))) { rec.Draw(); using (var rt = rec.GetPngTexture()) { rt.GetData(buffer); //比较上一帧 if (bufferPrev != null) { //跳过当前帧 if (memcmp(buffer, bufferPrev, (IntPtr)buffer.Length) == 0) { continue; } else //计算时间 输出 { writeFrame(d); } } else { bufferPrev = new byte[bounds.Value.Width * bounds.Value.Height * 4]; } //交换缓冲区 var temp = buffer; buffer = bufferPrev; bufferPrev = temp; prevTime = d; //透明 额外导出一份gif if (!encParams.SupportAlphaChannel && config.BackgroundType.Value == ImageBackgroundType.Transparent) { using (var rt2 = rec.GetGifTexture(config.BackgroundColor.Value.ToXnaColor(), config.MinMixedAlpha)) { if (bufferGif == null) { bufferGif = new byte[bufferPrev.Length]; } rt2.GetData(bufferGif); } } else { bufferGif = bufferPrev; } } } //输出最后一帧 if (prevTime < length) { writeFrame(length); } else if (length <= 0) //0长度补丁 { writeFrame(100); } //保存动画长度 if (config.SavePngFramesEnabled) { File.WriteAllText(Path.Combine(dirName, "delay.txt"), length.ToString()); } rec.End(); enc.Dispose(); }