private void SaveAsGif <T>(string file, Size size, Size maxSize, int frames, int frameDelay) where T : unmanaged, IPixel <T> { // Load full base image using Image <T> image = Image.LoadPixelData <T>(_ddsImageFile.Data, _ddsImageFile.Width, _ddsImageFile.Height); using Image <T> gif = new Image <T>(size.Width, size.Height); for (int i = 0; i < frames; i++) { #pragma warning disable SA1407 // Arithmetic expressions should declare precedence int xPos = i % (image.Width / maxSize.Width) * maxSize.Width; #pragma warning restore SA1407 // Arithmetic expressions should declare precedence int yPos = i / (image.Width / maxSize.Width) * maxSize.Height; using Image <T> imagePart = image.Clone(); imagePart.Mutate(x => x.Crop(new Rectangle(new Point(xPos, yPos), size))); GifFrameMetadata gifFrameMetadata = imagePart.Frames[0].Metadata.GetFormatMetadata(GifFormat.Instance); gifFrameMetadata.FrameDelay = frameDelay / 10; gifFrameMetadata.DisposalMethod = GifDisposalMethod.RestoreToBackground; gif.Frames.AddFrame(imagePart.Frames[0]); } gif.Frames.RemoveFrame(0); gif.Save(file, new GifEncoder() { ColorTableMode = GifColorTableMode.Local, }); }
private static void CompareGifMetadata(ImageFrame a, ImageFrame b) { // TODO: all metadata classes should be equatable! GifFrameMetadata aData = a.Metadata.GetGifMetadata(); GifFrameMetadata bData = b.Metadata.GetGifMetadata(); Assert.Equal(aData.DisposalMethod, bData.DisposalMethod); Assert.Equal(aData.FrameDelay, bData.FrameDelay); Assert.Equal(aData.ColorTableLength, bData.ColorTableLength); }
private static GifFrameMetadata GetGifFrameMetadata(BitmapFrame bitmapFrame) { var gifFrameMetadata = new GifFrameMetadata(); if (bitmapFrame.Metadata is BitmapMetadata bitmapMetadata) { var delay = bitmapMetadata.GetQueryOrDefault <ushort>("/grctlext/Delay"); gifFrameMetadata.Delay = TimeSpan.FromMilliseconds(delay * 10); var disposal = bitmapMetadata.GetQueryOrDefault <byte>("/grctlext/Disposal"); gifFrameMetadata.Disposal = (FrameDisposalMethod)disposal; gifFrameMetadata.Left = bitmapMetadata.GetQueryOrDefault <ushort>("/imgdesc/Left"); gifFrameMetadata.Top = bitmapMetadata.GetQueryOrDefault <ushort>("/imgdesc/Top"); gifFrameMetadata.Width = bitmapMetadata.GetQueryOrDefault <ushort>("/imgdesc/Width"); gifFrameMetadata.Height = bitmapMetadata.GetQueryOrDefault <ushort>("/imgdesc/Height"); } return(gifFrameMetadata); }
public void NonMutatingEncodePreservesPaletteCount() { using (var inStream = new MemoryStream(TestFile.Create(TestImages.Gif.Leo).Bytes)) using (var outStream = new MemoryStream()) { inStream.Position = 0; var image = Image.Load <Rgba32>(inStream); GifMetadata metaData = image.Metadata.GetGifMetadata(); GifFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetGifMetadata(); GifColorTableMode colorMode = metaData.ColorTableMode; var encoder = new GifEncoder { ColorTableMode = colorMode, Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = frameMetadata.ColorTableLength }) }; image.Save(outStream, encoder); outStream.Position = 0; outStream.Position = 0; var clone = Image.Load <Rgba32>(outStream); GifMetadata cloneMetadata = clone.Metadata.GetGifMetadata(); Assert.Equal(metaData.ColorTableMode, cloneMetadata.ColorTableMode); // Gifiddle and Cyotek GifInfo say this image has 64 colors. Assert.Equal(64, frameMetadata.ColorTableLength); for (int i = 0; i < image.Frames.Count; i++) { GifFrameMetadata ifm = image.Frames[i].Metadata.GetGifMetadata(); GifFrameMetadata cifm = clone.Frames[i].Metadata.GetGifMetadata(); Assert.Equal(ifm.ColorTableLength, cifm.ColorTableLength); Assert.Equal(ifm.FrameDelay, cifm.FrameDelay); } image.Dispose(); clone.Dispose(); } }
public void CloneIsDeep() { var meta = new GifFrameMetadata() { FrameDelay = 1, DisposalMethod = GifDisposalMethod.RestoreToBackground, ColorTableLength = 2 }; var clone = (GifFrameMetadata)meta.DeepClone(); clone.FrameDelay = 2; clone.DisposalMethod = GifDisposalMethod.RestoreToPrevious; clone.ColorTableLength = 1; Assert.False(meta.FrameDelay.Equals(clone.FrameDelay)); Assert.False(meta.DisposalMethod.Equals(clone.DisposalMethod)); Assert.False(meta.ColorTableLength.Equals(clone.ColorTableLength)); }
public void ConstructorImageFrameMetadata() { const int frameDelay = 42; const int colorTableLength = 128; const GifDisposalMethod disposalMethod = GifDisposalMethod.RestoreToBackground; var metaData = new ImageFrameMetadata(); GifFrameMetadata gifFrameMetadata = metaData.GetGifMetadata(); gifFrameMetadata.FrameDelay = frameDelay; gifFrameMetadata.ColorTableLength = colorTableLength; gifFrameMetadata.DisposalMethod = disposalMethod; var clone = new ImageFrameMetadata(metaData); GifFrameMetadata cloneGifFrameMetadata = clone.GetGifMetadata(); Assert.Equal(frameDelay, cloneGifFrameMetadata.FrameDelay); Assert.Equal(colorTableLength, cloneGifFrameMetadata.ColorTableLength); Assert.Equal(disposalMethod, cloneGifFrameMetadata.DisposalMethod); }
private static void ConvertGifToMode2(string sourceFile, string outputPath) { string newPath = Path.Combine(outputPath, Path.GetFileNameWithoutExtension(sourceFile)); if (File.Exists(newPath)) { return; } DateTime modificationTime = File.GetLastWriteTimeUtc(sourceFile); Image <Rgba32> gif = Image.Load <Rgba32>(sourceFile); int height = 256; int width = 160; int frameCount = gif.Frames.Count; // Currently use ints to hold a list of pixel colours for a pixel. Each takes three bits, // so can only do 10. Easily extended with a long though. Debug.Assert(frameCount <= 10); if (frameCount > 10) { throw new ApplicationException("The source image has more than 10 frames"); } // These contains every observed pattern of colour cycling HashSet <int> staticCycles = new HashSet <int>(); // Cycles where the colour doesn't change HashSet <int> colourCycles = new HashSet <int>(); // Cycles where the colour changes byte[, ,] frames = new byte[frameCount, width, height]; int frameIndex = 0; bool patchWarning = false; foreach (ImageFrame <Rgba32> frame in gif.Frames) { Debug.Assert(frame.Width == width * 4); Debug.Assert(frame.Height == height * 2); if (frame.Width != width * 4) { throw new ApplicationException(string.Format("The source image is not {0} pixels wide", width * 4)); } if (frame.Height != height * 2) { throw new ApplicationException(string.Format("The source image is not {0} pixels high", height * 2)); } for (int y = 0; y != height; ++y) { for (int x = 0; x != width; ++x) { int xs = 4 * x; int ys = 2 * y; int r = 0; int g = 0; int b = 0; Color c = frame[xs, ys]; for (int y0 = 0; y0 != 2; ++y0) { for (int x0 = 0; x0 != 4; ++x0) { Color c2 = frame[xs + x0, ys + y0]; Rgba32 p = c2.ToPixel <Rgba32>(); Debug.Assert(c == c2); if (c2 != c) { patchWarning = true; } r += p.R; g += p.G; b += p.B; } } frames[frameIndex, x, y] = (byte)BeebColour(8 * 128, r, g, b); } } ++frameIndex; } if (patchWarning) { Console.WriteLine("img2beeb: WARNING: A 2x4 patch contains varying colours; is this a MODE2 image?"); } // Calculate vector of pixel colours across frames; // remember each unique vector. for (int y = 0; y != height; ++y) { for (int x = 0; x != width; ++x) { byte colour = frames[0, x, y]; int vector = colour; bool match = true; for (int frame = 1; frame != frameCount; ++frame) { byte current = frames[frame, x, y]; vector = (vector << 3) | current; match = match && (colour == current); } if (match) { staticCycles.Add(vector); } else { colourCycles.Add(vector); } } } Debug.Assert(staticCycles.Count + colourCycles.Count <= 16); if (staticCycles.Count + colourCycles.Count > 16) { throw new ApplicationException("The animation requires more than 16 colours"); } // These indices could be assigned on a single pass through the data, // but doing it at the end means they can be sorted. List <int> colourCyclesSorted = colourCycles.ToList(); colourCyclesSorted.Sort(); List <int> staticCyclesSorted = staticCycles.ToList(); staticCyclesSorted.Sort(); Dictionary <int, byte> colourNumbers = new Dictionary <int, byte>(); foreach (int cycle in staticCyclesSorted) { colourNumbers.Add(cycle, (byte)(colourNumbers.Count)); } foreach (int cycle in colourCyclesSorted) { colourNumbers.Add(cycle, (byte)(colourNumbers.Count)); } byte[,] final = new byte[width, height]; for (int y = 0; y != height; ++y) { for (int x = 0; x != width; ++x) { // Create vector of observed colours across frames int vector = 0; for (int frame = 0; frame != frameCount; ++frame) { vector = (vector << 3) | frames[frame, x, y]; } final[x, y] = colourNumbers[vector]; } } int rowCount = (height + 7) / 8; int rowLength = 640; byte[] mode2 = new byte[rowLength * rowCount]; for (int y = 0; y != height; ++y) { int rowStart = (y / 8) * rowLength + (y % 8); for (int x = 0; x != width; ++x) { // Source pixels are doubled up so each byte has two pixels the same colour int sourcePixel = final[x, y]; Debug.Assert(sourcePixel < 16); byte mask = (x % 2) == 0 ? (byte)0xAA : (byte)0x55; byte colour = (byte)(mode2ColourTable[sourcePixel] & mask); int destIndex = rowStart + (x / 2) * 8; mode2[destIndex] |= colour; } } byte[] palette = new byte[256]; byte[] message = Encoding.ASCII.GetBytes("Mode2 Animation"); message.CopyTo(palette, 0); int index = message.Length + 1; palette[index++] = (byte)frameCount; palette[index++] = (byte)staticCycles.Count; palette[index++] = (byte)colourCycles.Count; for (int colour = 0; colour != staticCyclesSorted.Count; ++colour) { palette[index++] = (byte)(staticCyclesSorted[colour] & 0x07); } for (int frame = 0; frame != frameCount; ++frame) { for (int colour = 0; colour != colourCyclesSorted.Count; ++colour) { palette[index++] = (byte)((colourCyclesSorted[colour] >> (3 * (frameCount - frame - 1))) & 0x07); } // Get frame delay in centiseconds GifFrameMetadata fmd = gif.Frames[frame].Metadata.GetFormatMetadata(GifFormat.Instance); int frameDelay = fmd.FrameDelay; Debug.Assert(frameDelay >= 0 && frameDelay < 256); if (frameDelay >= 256) { throw new ApplicationException("The frame delay is too long!"); } palette[index++] = (byte)frameDelay; } using (FileStream fs = new FileStream(newPath, FileMode.CreateNew, FileAccess.Write)) { fs.Write(palette, 0, palette.Length); fs.Write(mode2, 0, mode2.Length); } File.SetLastWriteTimeUtc(newPath, modificationTime); }
private static void GifDecodingCompleteCallback(int taskId) { if (!DecodeTasks.ContainsKey(taskId) || DecodeTasks[taskId] == null) { Debug.LogErrorFormat("Complete decoding GIF image with invalid task ID {0}. Please check!", taskId); return; } var taskResources = DecodeTasks[taskId]; GifMetadata gifMetadata; GifFrameMetadata[] gifFrameMetadata = null; Color32[][] imageData = null; try { gifMetadata = (GifMetadata)taskResources.gifMetadataHandle.Target; if (taskResources.frameMetadataHandles != null || taskResources.imageDataHandles != null) { imageData = new Color32[taskResources.imageDataHandles.Length][]; gifFrameMetadata = new GifFrameMetadata[taskResources.frameMetadataHandles.Length]; for (int i = 0; i < taskResources.frameMetadataHandles.Length; i++) { gifFrameMetadata[i] = (GifFrameMetadata)taskResources.frameMetadataHandles[i].Target; } for (int j = 0; j < taskResources.imageDataHandles.Length; j++) { imageData[j] = (Color32[])taskResources.imageDataHandles[j].Target; } } if (taskResources.completeCallback != null) { var callback = taskResources.completeCallback; // cache the callback reference as we'll reset taskResources soon. RuntimeHelper.RunOnMainThread(() => { callback(taskId, gifMetadata, gifFrameMetadata, imageData); }); } } catch (System.InvalidCastException e) { Debug.LogError("Error casting GCHandle back to decoding buffer:" + e); throw e; } finally { taskResources.gifMetadataHandle.Free(); if (taskResources.frameMetadataHandles != null) { foreach (var handle in taskResources.frameMetadataHandles) { handle.Free(); } } if (taskResources.imageDataHandles != null) { foreach (var handle in taskResources.imageDataHandles) { handle.Free(); } } taskResources = null; DecodeTasks.Remove(taskId); } }
/// <summary> /// Initializes a new instance of the <see cref="GifFrameMetadata"/> class. /// </summary> /// <param name="other">The metadata to create an instance from.</param> private GifFrameMetadata(GifFrameMetadata other) { this.ColorTableLength = other.ColorTableLength; this.FrameDelay = other.FrameDelay; this.DisposalMethod = other.DisposalMethod; }
public RomInfo(string decPath, bool parseImages = false) { byte[] contentBytes = File.ReadAllBytes(decPath); ushort headerChecksum = CalcCrc16Modbus(contentBytes.Slice(0, HeaderChecksumOffset)); ushort romChecksumValue = BitConverter.ToUInt16(contentBytes.Slice(HeaderChecksumOffset, 2, true).Reverse().ToArray()); ValidContent = headerChecksum == romChecksumValue; if (!ValidContent) { return; } GameTitle = new string(contentBytes.Slice(GameTitleOffset, GameTitleSize).AsChars()).TrimEnd('\0'); GameCode = new string(contentBytes.Slice(GameCodeOffset, GameCodeSize).AsChars()); if (GameCode.Length >= GameCodeLength) { RegionCode = GameCode[GameCodeLength - 1]; } else { Log.Instance.Warn($"The gamecode for '{decPath}' is not valid: '{GameCode}'"); } int titleInfoAddress = BitConverter.ToInt32(contentBytes.Slice(TitleInfoAddressOffset, TitleInfoAddressSize, true).Reverse().ToArray()); if (titleInfoAddress == 0x00000000) { return; } TitleInfoVersion = (TitleInfoVersions)BitConverter.ToInt16(contentBytes.Slice(titleInfoAddress, 2, true).Reverse().ToArray()); UnicodeEncoding encoding = new UnicodeEncoding(false, false); int titleCount = TitleInfoVersion == TitleInfoVersions.Original ? 6 : TitleInfoVersion == TitleInfoVersions.ChineseTitle ? 7 : 8; for (int i = 0; i < titleCount; i++) { Titles[i] = encoding.GetString(contentBytes.Slice(titleInfoAddress + TitleInfoTitlesOffset + TitleInfoTitleSize * i, TitleInfoTitleSize)).Trim('\0', '\uffff').AsNullIfEmpty(); } if (!parseImages) { return; } // Static icon StaticIcon = BuildImage(ParseBitmapIndices(contentBytes.Slice(titleInfoAddress + TitleInfoStaticIconOffset, IconBitmapSize)), ParsePalette(contentBytes.Slice(titleInfoAddress + TitleInfoStaticIconOffset + IconBitmapSize, IconPaletteBytesPerColour * IconPaletteColours))); PngMetadata pngMetadata = StaticIcon.Metadata.GetFormatMetadata(PngFormat.Instance); pngMetadata.ColorType = PngColorType.Palette; pngMetadata.TextData = new List <PngTextData> { new PngTextData("description", $"Icon for the DSiWare title \"{GetFriendlyTitle()}\" (game code \"{GameCode}\").", "en-ca", "") }; // Animated icon if (TitleInfoVersion != TitleInfoVersions.ChineseKoreanTitlesAnimated) { return; } byte[] sequenceBytes = contentBytes.Slice(titleInfoAddress + TitleInfoAnimIconSequenceOffset, TitleInfoAnimIconSequenceFrameCount * 2); // Non-animated icons start with 0x01000001 if (sequenceBytes[0] == 0x01 && sequenceBytes[1] == 0x00 && sequenceBytes[2] == 0x00 && sequenceBytes[3] == 0x01) { return; } byte[][] bitmapPaletteIndices = new byte[TitleInfoAnimIconBitmapCount][]; for (int i = 0; i < TitleInfoAnimIconBitmapCount; i++) { bitmapPaletteIndices[i] = ParseBitmapIndices(contentBytes.Slice( titleInfoAddress + TitleInfoAnimIconBitmapsOffset + i * IconBitmapSize, IconBitmapSize)); } Rgba32[][] paletteColours = new Rgba32[TitleInfoAnimIconPaletteCount][]; for (int i = 0; i < TitleInfoAnimIconPaletteCount; i++) { paletteColours[i] = ParsePalette(contentBytes.Slice( titleInfoAddress + TitleInfoAnimIconPalettesOffset + i * IconPaletteBytesPerColour * IconPaletteColours, IconPaletteBytesPerColour * IconPaletteColours)); } AnimatedIcon = new Image <Rgba32>(IconSquareDim, IconSquareDim); for (int i = 0; i < TitleInfoAnimIconSequenceFrameCount; i++) { if (sequenceBytes[i * 2] == 0x00 && sequenceBytes[i * 2 + 1] == 0x00) { break; } bool flipVertical = (sequenceBytes[i * 2 + 1] & 0b10000000) >> 7 == 1; bool flipHorizontal = (sequenceBytes[i * 2 + 1] & 0b01000000) >> 6 == 1; byte paletteIndex = (byte)((sequenceBytes[i * 2 + 1] & 0b00111000) >> 3); byte bitmapIndex = (byte)(sequenceBytes[i * 2 + 1] & 0b00000111); byte frameDuration = sequenceBytes[i * 2]; // in 60Hz units Image <Rgba32> frame = BuildImage(bitmapPaletteIndices[bitmapIndex], paletteColours[paletteIndex], flipHorizontal, flipVertical); ImageFrame <Rgba32> aFrame = AnimatedIcon.Frames.AddFrame(frame.Frames.RootFrame); GifFrameMetadata frameMetadata = aFrame.Metadata.GetFormatMetadata(GifFormat.Instance); frameMetadata.FrameDelay = (int)Math.Round(frameDuration * FrameDurationConversion); frameMetadata.DisposalMethod = GifDisposalMethod.RestoreToBackground; } AnimatedIcon.Frames.RemoveFrame(0); AnimatedIcon.Metadata.GetFormatMetadata(GifFormat.Instance).RepeatCount = 0; GifMetadata gifMetadata = AnimatedIcon.Metadata.GetFormatMetadata(GifFormat.Instance); gifMetadata.RepeatCount = 0; gifMetadata.Comments = new List <string> { $"Icon for the DSiWare title \"{GetFriendlyTitle()}\" (game code \"{GameCode}\")." }; }