/// <summary> /// Converts the list of frames into an animated GIF, and writes it to the stream. /// </summary> /// <param name="stream">Output stream.</param> public void Save(Stream stream, out int maxWidth, out int maxHeight) { maxWidth = maxHeight = -1; if (Frames.Count == 0) { // nothing to do Debug.Assert(false); return; } // // Step 1: convert all BitmapFrame objects to GIF. This lets the .NET GIF encoder // deal with the data compression. // List <UnpackedGif> gifs = new List <UnpackedGif>(Frames.Count); foreach (BitmapFrame bf in Frames) { GifBitmapEncoder encoder = new GifBitmapEncoder(); encoder.Frames.Add(bf); using (MemoryStream ms = new MemoryStream()) { encoder.Save(ms); // We're using GetBuffer() rather than ToArray() to avoid a copy. One // consequence of this choice is that the byte[] may be oversized. Since // GIFs are treated as streams with explicit termination this should not // pose a problem. gifs.Add(UnpackedGif.Create(ms.GetBuffer())); } } // // Step 2: determine the size of the largest image. This will become the logical // size of the animated GIF. // // TODO(maybe): We have an opportunity to replace all of the local color tables with a // single global color table. This is only possible if all of the local tables are // identical and the transparency values in the GCE also match up. (Well, it's // otherwise *possible*, but we'd need to decode, update palettes and pixels, and // re-encode.) // foreach (UnpackedGif gif in gifs) { //gif.DebugDump(); if (maxWidth < gif.LogicalScreenWidth) { maxWidth = gif.LogicalScreenWidth; } if (maxHeight < gif.LogicalScreenHeight) { maxHeight = gif.LogicalScreenHeight; } } if (maxWidth < 0 || maxHeight < 0) { Debug.WriteLine("Unable to determine correct width/height"); return; } // // Step 3: output data. // stream.Write(GIF89A_SIGNATURE, 0, GIF89A_SIGNATURE.Length); WriteLittleUshort(stream, (ushort)maxWidth); WriteLittleUshort(stream, (ushort)maxHeight); stream.WriteByte(0x70); // no GCT; max color resolution (does this matter?) stream.WriteByte(0); // BCI; not relevant stream.WriteByte(0); // no aspect ratio adjustment stream.Write(NetscapeExtStart, 0, NetscapeExtStart.Length); stream.WriteByte(1); // yes, we want to loop WriteLittleUshort(stream, 0); // loop forever stream.WriteByte(0); // end of block Debug.Assert(gifs.Count == FrameData.Count); for (int i = 0; i < Frames.Count; i++) { UnpackedGif gif = gifs[i]; MetaData md = FrameData[i]; // Just use the first image. UnpackedGif.GraphicRenderingBlock grb = gif.ImageBlocks[0]; byte colorTableSize; byte[] colorTable; if (grb.LocalColorTableFlag) { colorTableSize = grb.LocalColorTableSize; colorTable = grb.LocalColorTable; } else if (gif.GlobalColorTableFlag) { colorTableSize = gif.GlobalColorTableSize; colorTable = gif.GlobalColorTable; } else { Debug.Assert(false); colorTableSize = 0x07; colorTable = new byte[256 * 3]; // a whole lotta black } Debug.Assert(colorTable.Length == (1 << (colorTableSize + 1)) * 3); // If it has a GCE, use that. Otherwise supply default values. Either way // we use the frame delay from the meta-data. UnpackedGif.GraphicControlExtension gce = grb.GraphicControlExt; byte disposalMethod = (byte)UnpackedGif.GraphicControlExtension.DisposalMethods.RestoreBackground; bool userInputFlag = false; bool transparencyFlag = false; byte transparentColorIndex = 0; if (gce != null) { //disposalMethod = gce.DisposalMethod; userInputFlag = gce.UserInputFlag; transparencyFlag = gce.TransparencyFlag; transparentColorIndex = gce.TransparentColorIndex; } stream.Write(GraphicControlStart, 0, GraphicControlStart.Length); stream.WriteByte((byte)((disposalMethod << 2) | (userInputFlag ? 0x02 : 0) | (transparencyFlag ? 0x01 : 0))); WriteLittleUshort(stream, (ushort)Math.Round(md.DelayMsec / 10.0)); stream.WriteByte(transparentColorIndex); stream.WriteByte(0); // end of GCE // Output image descriptor. We can center the images in the animation or // just leave them in the top-left corner. stream.WriteByte(UnpackedGif.IMAGE_SEPARATOR); WriteLittleUshort(stream, 0); // left WriteLittleUshort(stream, 0); // top WriteLittleUshort(stream, gif.LogicalScreenWidth); WriteLittleUshort(stream, gif.LogicalScreenHeight); stream.WriteByte((byte)(0x80 | colorTableSize)); // local table, no sort/intrl // Local color table. stream.Write(colorTable, 0, colorTable.Length); // Image data. Trailing $00 is included. stream.Write(grb.ImageData, grb.ImageStartOffset, grb.ImageEndOffset - grb.ImageStartOffset + 1); } stream.WriteByte(UnpackedGif.GIF_TRAILER); }
/// <summary> /// Generate one or more GIF image files, and output references to them. /// </summary> /// <param name="offset">Visualization set file offset.</param> /// <param name="sb">String builder for the HTML output.</param> private void OutputVisualizationSet(int offset, StringBuilder sb) { const int IMAGE_SIZE = 64; const int MAX_WIDTH_PER_LINE = 768; if (!mProject.VisualizationSets.TryGetValue(offset, out VisualizationSet visSet)) { sb.Append("Internal error - visualization set missing"); Debug.Assert(false); return; } if (visSet.Count == 0) { sb.Append("Internal error - empty visualization set"); Debug.Assert(false); return; } string imageDirFileName = Path.GetFileName(mImageDirPath); int outputWidth = 0; for (int index = 0; index < visSet.Count; index++) { string fileName = "vis" + offset.ToString("x6") + "_" + index.ToString("d2"); int dispWidth, dispHeight; Visualization vis = visSet[index]; if (vis is VisualizationAnimation) { // Animated visualization. VisualizationAnimation visAnim = (VisualizationAnimation)vis; int frameDelay = PluginCommon.Util.GetFromObjDict(visAnim.VisGenParams, VisualizationAnimation.FRAME_DELAY_MSEC_PARAM, 330); AnimatedGifEncoder encoder = new AnimatedGifEncoder(); // Gather list of frames. for (int i = 0; i < visAnim.Count; i++) { Visualization avis = VisualizationSet.FindVisualizationBySerial( mProject.VisualizationSets, visAnim[i]); if (avis != null) { encoder.AddFrame(BitmapFrame.Create(avis.CachedImage), frameDelay); } else { Debug.Assert(false); // not expected } } #if false // try feeding the animated GIF into our GIF unpacker using (MemoryStream ms = new MemoryStream()) { encoder.Save(ms); Debug.WriteLine("TESTING"); UnpackedGif anim = UnpackedGif.Create(ms.GetBuffer()); anim.DebugDump(); } #endif // Create new or replace existing image file. fileName += "_ani.gif"; string pathName = Path.Combine(mImageDirPath, fileName); try { using (FileStream stream = new FileStream(pathName, FileMode.Create)) { encoder.Save(stream, out dispWidth, out dispHeight); } } catch (Exception ex) { // TODO: add an error report Debug.WriteLine("Error creating animated GIF file '" + pathName + "': " + ex.Message); dispWidth = dispHeight = 1; } } else { // Bitmap visualization. // // Encode a GIF the same size as the original bitmap. GifBitmapEncoder encoder = new GifBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(vis.CachedImage)); // Create new or replace existing image file. fileName += ".gif"; string pathName = Path.Combine(mImageDirPath, fileName); try { using (FileStream stream = new FileStream(pathName, FileMode.Create)) { encoder.Save(stream); } } catch (Exception ex) { // Something went wrong with file creation. We don't have an error // reporting mechanism, so this will just appear as a broken or stale // image reference. // TODO: add an error report Debug.WriteLine("Error creating GIF file '" + pathName + "': " + ex.Message); } dispWidth = (int)vis.CachedImage.Width; dispHeight = (int)vis.CachedImage.Height; } // Output thumbnail-size IMG element, preserving proportions. I'm assuming // images will be small enough that generating a separate thumbnail would be // counter-productive. This seems to look best if the height is consistent // across all visualization lines, but that can create some monsters (e.g. // a bitmap that's 1 pixel high and 40 wide), so we cap the width. int dimMult = IMAGE_SIZE; double maxDim = dispHeight; if (dispWidth > dispHeight * 2) { // Too proportionally wide, so use the width as the limit. Allow it to // up to 2x the max width (which can't cause the thumb height to exceed // the height limit). maxDim = dispWidth; dimMult *= 2; } int thumbWidth = (int)Math.Round(dimMult * (dispWidth / maxDim)); int thumbHeight = (int)Math.Round(dimMult * (dispHeight / maxDim)); //Debug.WriteLine(dispWidth + "x" + dispHeight + " --> " + // thumbWidth + "x" + thumbHeight + " (" + maxDim + ")"); if (outputWidth > MAX_WIDTH_PER_LINE) { // Add a line break. In "pre" mode the bitmaps just run off the right // edge of the screen. The way we're doing it is imprecise and doesn't // flow with changes to the browser width, but it'll do for now. sb.AppendLine("<br/>"); for (int i = 0; i < mColStart[(int)Col.Label]; i++) { sb.Append(' '); } outputWidth = 0; } else if (index != 0) { sb.Append(" "); } outputWidth += thumbWidth; sb.Append("<img class=\"vis\" alt=\"vis\" src=\""); sb.Append(imageDirFileName); sb.Append('/'); sb.Append(fileName); sb.Append("\" width=\"" + thumbWidth + "\" height=\"" + thumbHeight + "\"/>"); } }
private void SaveButton_Click(object sender, RoutedEventArgs e) { SaveFileDialog fileDlg = new SaveFileDialog() { Filter = Res.Strings.FILE_FILTER_GIF + "|" + Res.Strings.FILE_FILTER_ALL, FilterIndex = 1, ValidateNames = true, AddExtension = true, FileName = mFileNameBase + ".gif" }; if (fileDlg.ShowDialog() != true) { return; } string pathName = Path.GetFullPath(fileDlg.FileName); Debug.WriteLine("Save path: " + pathName); try { OutputSize item = (OutputSize)sizeComboBox.SelectedItem; if (mVis is VisWireframeAnimation) { Debug.Assert(item.Width == item.Height); AnimatedGifEncoder encoder = new AnimatedGifEncoder(); ((VisWireframeAnimation)mVis).EncodeGif(encoder, item.Width); using (FileStream stream = new FileStream(pathName, FileMode.Create)) { encoder.Save(stream, out int dispWidth, out int dispHeight); } } else { BitmapSource outImage; if (IsBitmap) { int scale = item.Width / (int)mVis.CachedImage.Width; Debug.Assert(scale >= 1); if (scale == 1) { outImage = mVis.CachedImage; } else { outImage = mVis.CachedImage.CreateScaledCopy(scale); } } else { Debug.Assert(item.Width == item.Height); outImage = Visualization.GenerateWireframeImage(mWireObj, item.Width, mVis.VisGenParams); } GifBitmapEncoder encoder = new GifBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(outImage)); #if false // try feeding the GIF into our GIF unpacker using (MemoryStream ms = new MemoryStream()) { encoder.Save(ms); Debug.WriteLine("TESTING"); UnpackedGif anim = UnpackedGif.Create(ms.GetBuffer()); anim.DebugDump(); } #else using (FileStream stream = new FileStream(pathName, FileMode.Create)) { encoder.Save(stream); } #endif } } catch (Exception ex) { // Error handling is a little sloppy, but this shouldn't fail often. MessageBox.Show(ex.Message, Res.Strings.ERR_FILE_GENERIC_CAPTION, MessageBoxButton.OK, MessageBoxImage.Error); return; } // After successful save, close dialog box. DialogResult = true; }