From 7e69ae74836b6eae583f5565716b15715cb2bac8 Mon Sep 17 00:00:00 2001 From: AsY!um- <377468+AsYlum-@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:55:30 +0100 Subject: [PATCH 1/5] Fix empty frames when adding GIFs in animation edit Convert each GIF frame instead of a blank bitmap, and keep all frames of the GIF instead of a one-frame copy. --- UoFiddler.Controls/Forms/AnimationEditForm.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/UoFiddler.Controls/Forms/AnimationEditForm.cs b/UoFiddler.Controls/Forms/AnimationEditForm.cs index 89006bfc..a7730fab 100644 --- a/UoFiddler.Controls/Forms/AnimationEditForm.cs +++ b/UoFiddler.Controls/Forms/AnimationEditForm.cs @@ -1418,11 +1418,12 @@ private void OnClickAdd(object sender, EventArgs e) { using (var bmpTemp = new Bitmap(fileName)) { - Bitmap bitmap = new Bitmap(bmpTemp); + // Copying a GIF keeps only its first frame + Bitmap bitmap = fileName.Contains(".gif") ? bmpTemp : new Bitmap(bmpTemp); - if (dialog.FileName.Contains(".bmp") || dialog.FileName.Contains(".tiff") || - dialog.FileName.Contains(".png") || dialog.FileName.Contains(".jpeg") || - dialog.FileName.Contains(".jpg")) + if (fileName.Contains(".bmp") || fileName.Contains(".tiff") || + fileName.Contains(".png") || fileName.Contains(".jpeg") || + fileName.Contains(".jpg")) { bitmap = ConvertBmpAnim(bitmap, (int)numericUpDownRed.Value, (int)numericUpDownGreen.Value, (int)numericUpDownBlue.Value); @@ -1436,7 +1437,7 @@ private void OnClickAdd(object sender, EventArgs e) } //Gif Especial Properties - if (dialog.FileName.Contains(".gif")) + if (fileName.Contains(".gif")) { FrameDimension dimension = new FrameDimension(bitmap.FrameDimensionsList[0]); @@ -1525,9 +1526,8 @@ private void AddImageAtCertainIndex(int frameCount, Bitmap[] bitBmp, Bitmap bmp, // Return an Image at a certain index for (int index = 0; index < frameCount; index++) { - bitBmp[index] = new Bitmap(bmp.Width, bmp.Height, PixelFormat.Format16bppArgb1555); bmp.SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnim(bitBmp[index], (int) numericUpDownRed.Value, + bitBmp[index] = ConvertBmpAnim(bmp, (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, (int) numericUpDownBlue.Value); edit.AddFrame(bitBmp[index], bitBmp[index].Width / 2); TreeNode node = GetNode(_currentBody); @@ -2036,10 +2036,8 @@ private void AddFilesAllDirections(OpenFileDialog dialog) if (dialog.FileNames[w].Contains(".gif")) { // dialog.Filename replaced by dialog.FileNames[w] - using (Bitmap bmpTemp = new Bitmap(dialog.FileNames[w])) + using (Bitmap bitmap = new Bitmap(dialog.FileNames[w])) { - Bitmap bitmap = new Bitmap(bmpTemp); - FrameDimension dimension = new FrameDimension(bitmap.FrameDimensionsList[0]); // Number of frames From 8199fd36432aca0b9a9a0a5cf1a72ec27c658294 Mon Sep 17 00:00:00 2001 From: AsY!um- <377468+AsYlum-@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:56:38 +0100 Subject: [PATCH 2/5] Keep near-white and near-grey pixels in GIF animation import Match background colours on the source colour, not after the drop to 5 bits, which made pixels of 248-255 white (and 168-175 grey in CV5/KR) transparent. --- UoFiddler.Controls/Forms/AnimationEditForm.cs | 162 ++++++------------ 1 file changed, 50 insertions(+), 112 deletions(-) diff --git a/UoFiddler.Controls/Forms/AnimationEditForm.cs b/UoFiddler.Controls/Forms/AnimationEditForm.cs index a7730fab..5fd5123f 100644 --- a/UoFiddler.Controls/Forms/AnimationEditForm.cs +++ b/UoFiddler.Controls/Forms/AnimationEditForm.cs @@ -2602,7 +2602,7 @@ private void AddAnimationX1(Color customConvert, Bitmap bmp) { Rectangle rect = new Rectangle(left, top, bitBmp[index].Width - left - right, bitBmp[index].Height - top - bottom); bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = bitBmp[index].Clone(rect, PixelFormat.Format16bppArgb1555); + bitBmp[index] = bitBmp[index].Clone(rect, PixelFormat.Format32bppArgb); } // End of Canvas algorithm @@ -2735,14 +2735,27 @@ private void OnClickGeneratePalette(object sender, EventArgs e) } //End of Soulblighter Modification - private static unsafe Bitmap ConvertBmpAnim(Bitmap bmp, int red, int green, int blue) + private static Bitmap ConvertBmpAnim(Bitmap bmp, int red, int green, int blue) { - //Extra background - int extraBack = (red / 8 * 1024) + (green / 8 * 32) + (blue / 8) + 32768; + return ConvertToAnimFrame(bmp, red, green, blue, removeGrey: false, removeGreen: false); + } - BitmapData bd = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format16bppArgb1555); - ushort* line = (ushort*)bd.Scan0; - int delta = bd.Stride >> 1; + /// + /// Converts an image to an ARGB1555 animation frame. Pure white, the custom background colour and, + /// with , 170,170,170 become transparent. They are matched on the + /// source 8-bit colour; matching after the drop to 5 bits also removed near-white and near-grey pixels. + /// + private static unsafe Bitmap ConvertToAnimFrame(Bitmap bmp, int red, int green, int blue, bool removeGrey, bool removeGreen) + { + const int white = 0xFFFFFF; + int grey = _greyConvert.ToArgb() & 0xFFFFFF; + int custom = (red << 16) | (green << 8) | blue; + // A background that is black after the drop to 5 bits was never removed; black is kept visible below + bool useCustom = (red >> 3) != 0 || (green >> 3) != 0 || (blue >> 3) != 0; + + BitmapData bd = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + uint* line = (uint*)bd.Scan0; + int delta = bd.Stride >> 2; Bitmap bmpNew = new Bitmap(bmp.Width, bmp.Height, PixelFormat.Format16bppArgb1555); BitmapData bdNew = bmpNew.LockBits(new Rectangle(0, 0, bmpNew.Width, bmpNew.Height), ImageLockMode.WriteOnly, PixelFormat.Format16bppArgb1555); @@ -2752,22 +2765,38 @@ private static unsafe Bitmap ConvertBmpAnim(Bitmap bmp, int red, int green, int for (int y = 0; y < bmp.Height; ++y, line += delta, lineNew += deltaNew) { - ushort* cur = line; + uint* cur = line; ushort* curNew = lineNew; for (int x = 0; x < bmp.Width; ++x) { - //My Soulblighter Modification - // Convert color 0,0,0 to 0,0,8 - if (cur[x] == 32768) + curNew[x] = 0; + + uint argb = cur[x]; + if ((argb >> 24) < 0x80) { - curNew[x] = 32769; + continue; } - if (cur[x] != 65535 && cur[x] != extraBack && cur[x] > 32768) //True White == BackGround + int rgb = (int)(argb & 0xFFFFFF); + if (rgb == white || (useCustom && rgb == custom) || (removeGrey && rgb == grey)) { - curNew[x] = cur[x]; + continue; } - //End of Soulblighter Modification + + int r = (rgb >> 19) & 0x1F; + int g = (rgb >> 11) & 0x1F; + int b = (rgb >> 3) & 0x1F; + + // KR frame viewer exports use a green background + if (removeGreen && g > b && g > r && g > 10) + { + continue; + } + + int color = 0x8000 | (r << 10) | (g << 5) | b; + + // 0,0,0 is transparent in the client, keep black visible as 0,0,8 + curNew[x] = (ushort)(color == 0x8000 ? 0x8001 : color); } } bmp.UnlockBits(bd); @@ -3231,44 +3260,9 @@ private AnimIdx Cv5AnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimens return edit; } - private static unsafe Bitmap ConvertBmpAnimCv5(Bitmap bmp, int red, int green, int blue) + private static Bitmap ConvertBmpAnimCv5(Bitmap bmp, int red, int green, int blue) { - //Extra background - int extraBack = (red / 8 * 1024) + (green / 8 * 32) + (blue / 8) + 32768; - - BitmapData bd = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format16bppArgb1555); - ushort* line = (ushort*)bd.Scan0; - int delta = bd.Stride >> 1; - - Bitmap bmpNew = new Bitmap(bmp.Width, bmp.Height, PixelFormat.Format16bppArgb1555); - BitmapData bdNew = bmpNew.LockBits(new Rectangle(0, 0, bmpNew.Width, bmpNew.Height), ImageLockMode.WriteOnly, PixelFormat.Format16bppArgb1555); - - ushort* lineNew = (ushort*)bdNew.Scan0; - int deltaNew = bdNew.Stride >> 1; - - for (int y = 0; y < bmp.Height; ++y, line += delta, lineNew += deltaNew) - { - ushort* cur = line; - ushort* curNew = lineNew; - for (int x = 0; x < bmp.Width; ++x) - { - //My Soulblighter Modification - // Convert color 0,0,0 to 0,0,8 - if (cur[x] == 32768) - { - curNew[x] = 32769; - } - - if (cur[x] != 65535 && cur[x] != 54965 && cur[x] != extraBack && cur[x] > 32768) //True White == BackGround - { - curNew[x] = cur[x]; - } - //End of Soulblighter Modification - } - } - bmp.UnlockBits(bd); - bmpNew.UnlockBits(bdNew); - return bmpNew; + return ConvertToAnimFrame(bmp, red, green, blue, removeGrey: true, removeGreen: false); } private static readonly Color _greyConvert = Color.FromArgb(255, 170, 170, 170); @@ -3607,7 +3601,7 @@ private static void Cv5ProcessFrames(Bitmap[] bitBmp, FrameDimension dimension, { Rectangle rect = new Rectangle(left, top, bitBmp[index].Width - left - right, bitBmp[index].Height - top - bottom); bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = bitBmp[index].Clone(rect, PixelFormat.Format16bppArgb1555); + bitBmp[index] = bitBmp[index].Clone(rect, PixelFormat.Format32bppArgb); } } @@ -3967,65 +3961,9 @@ private AnimIdx KrAnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimensi return edit; } - private static unsafe Bitmap ConvertBmpAnimKr(Bitmap bmp, int red, int green, int blue) + private static Bitmap ConvertBmpAnimKr(Bitmap bmp, int red, int green, int blue) { - // Extra background - int extraBack = (red / 8 * 1024) + (green / 8 * 32) + (blue / 8) + 32768; - - BitmapData bd = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format16bppArgb1555); - ushort* line = (ushort*)bd.Scan0; - int delta = bd.Stride >> 1; - - Bitmap bmpNew = new Bitmap(bmp.Width, bmp.Height, PixelFormat.Format16bppArgb1555); - BitmapData bdNew = bmpNew.LockBits(new Rectangle(0, 0, bmpNew.Width, bmpNew.Height), ImageLockMode.WriteOnly, PixelFormat.Format16bppArgb1555); - - ushort* lineNew = (ushort*)bdNew.Scan0; - int deltaNew = bdNew.Stride >> 1; - - for (int y = 0; y < bmp.Height; ++y, line += delta, lineNew += deltaNew) - { - ushort* cur = line; - ushort* curNew = lineNew; - for (int x = 0; x < bmp.Width; ++x) - { - //if (cur[X] != 53235) - //{ - // Convert back to RGB - int blueTemp = (cur[x] - 32768) / 32; - blueTemp *= 32; - blueTemp = cur[x] - 32768 - blueTemp; - - int greenTemp = (cur[x] - 32768) / 1024; - greenTemp *= 1024; - greenTemp = cur[x] - 32768 - greenTemp - blueTemp; - greenTemp /= 32; - - int redTemp = (cur[x] - 32768) / 1024; - - // remove green colors - if (greenTemp > blueTemp && greenTemp > redTemp && greenTemp > 10) - { - cur[x] = 65535; - } - //} - - //My Soulblighter Modification - // Convert color 0,0,0 to 0,0,8 - if (cur[x] == 32768) - { - curNew[x] = 32769; - } - - if (cur[x] != 65535 && cur[x] != 54965 && cur[x] != extraBack && cur[x] > 32768) //True White == BackGround - { - curNew[x] = cur[x]; - } - //End of Soulblighter Modification - } - } - bmp.UnlockBits(bd); - bmpNew.UnlockBits(bdNew); - return bmpNew; + return ConvertToAnimFrame(bmp, red, green, blue, removeGrey: true, removeGreen: true); } private static void KrCanvasAlgorithm(Bitmap[] bitBmp, int frameCount, FrameDimension dimension, Color customConvert) @@ -4350,7 +4288,7 @@ private static void KrProcessFrames(Bitmap[] bitBmp, FrameDimension dimension, C { Rectangle rect = new Rectangle(left, top, bitBmp[index].Width - left - right, bitBmp[index].Height - top - bottom); bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = bitBmp[index].Clone(rect, PixelFormat.Format16bppArgb1555); + bitBmp[index] = bitBmp[index].Clone(rect, PixelFormat.Format32bppArgb); } } From e28d9279fa1a9579e3c84286820173fd58073330 Mon Sep 17 00:00:00 2001 From: AsY!um- <377468+AsYlum-@users.noreply.github.com> Date: Thu, 12 Nov 2026 00:07:23 +0100 Subject: [PATCH 3/5] Build animation palettes from all frames on import (#28) Adding or replacing frames fits the palette to their colours, reducing past 256 and mapping to the nearest colour instead of index 0; existing frames keep their colours. --- Ultima/AnimationEdit.cs | 365 +++++++++++++++++- UoFiddler.Controls/Forms/AnimationEditForm.cs | 268 +++++-------- 2 files changed, 444 insertions(+), 189 deletions(-) diff --git a/Ultima/AnimationEdit.cs b/Ultima/AnimationEdit.cs index 86f0125c..53a829a8 100644 --- a/Ultima/AnimationEdit.cs +++ b/Ultima/AnimationEdit.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.IO; @@ -525,6 +526,7 @@ public void AddFrame(Bitmap bit, int centerX = 0, int centerY = 0 ) Frames = new List(); } + FitPalette(new[] { bit }); Frames.Add(new FrameEdit(bit, Palette, centerX, centerY)); } @@ -540,9 +542,315 @@ public void ReplaceFrame(Bitmap bit, int index) return; } + FitPalette(new[] { bit }, index); Frames[index] = new FrameEdit(bit, Palette, Frames[index].Center.X, Frames[index].Center.Y); } + /// + /// Makes the palette hold the colours of next to those of the current frames, + /// so they can be added without losing colours. New colours go into entries no frame uses; past 256 + /// colours the palette is reduced over all frames and the current frames are remapped to it. + /// Fitting all frames of an import at once gives a better reduction than adding them one by one. + /// + /// Frames about to be added or replaced. + /// Index of the frame being replaced, whose colours no longer count. + public void FitPalette(IEnumerable bitmaps, int replacedFrame = -1) + { + Dictionary newColors = CountColors(bitmaps); + if (newColors.Count == 0) + { + return; + } + + var usedCounts = new int[PaletteCapacity]; + if (Frames != null) + { + for (int f = 0; f < Frames.Count; ++f) + { + if (f == replacedFrame || Frames[f].RawData == null) + { + continue; + } + + foreach (FrameEdit.Raw raw in Frames[f].RawData) + { + foreach (byte index in raw.data) + { + usedCounts[index]++; + } + } + } + } + + var reserved = new bool[PaletteCapacity]; + for (int i = 0; i < PaletteCapacity; ++i) + { + reserved[i] = usedCounts[i] > 0; + } + + var missing = new List(); + foreach (ushort color in newColors.Keys) + { + int index = Array.IndexOf(Palette, color); + if (index >= 0) + { + reserved[index] = true; + } + else + { + missing.Add(color); + } + } + + if (missing.Count == 0) + { + return; + } + + // Empty entries first, then entries no frame uses + var free = new List(); + for (int i = 0; i < PaletteCapacity; ++i) + { + if (!reserved[i] && Palette[i] == 0x8000) + { + free.Add(i); + } + } + + for (int i = 0; i < PaletteCapacity; ++i) + { + if (!reserved[i] && Palette[i] != 0x8000) + { + free.Add(i); + } + } + + if (missing.Count <= free.Count) + { + for (int i = 0; i < missing.Count; ++i) + { + Palette[free[i]] = missing[i]; + } + + return; + } + + var histogram = new Dictionary(newColors); + for (int i = 0; i < PaletteCapacity; ++i) + { + if (usedCounts[i] == 0) + { + continue; + } + + ushort color = FrameEdit.VisibleColor(Palette[i]); + histogram.TryGetValue(color, out int count); + histogram[color] = count + usedCounts[i]; + } + + ushort[] reduced = MedianCut(histogram, PaletteCapacity); + + var remap = new byte[PaletteCapacity]; + for (int i = 0; i < PaletteCapacity; ++i) + { + remap[i] = FrameEdit.NearestIndex(reduced, FrameEdit.VisibleColor(Palette[i])); + } + + if (Frames != null) + { + foreach (FrameEdit frame in Frames) + { + if (frame.RawData == null) + { + continue; + } + + foreach (FrameEdit.Raw raw in frame.RawData) + { + for (int k = 0; k < raw.data.Length; ++k) + { + raw.data[k] = remap[raw.data[k]]; + } + } + } + } + + Array.Copy(reduced, Palette, PaletteCapacity); + } + + /// + /// Replaces the palette with the colours of , reduced to 256 when there are more. + /// Frames keep their palette indexes. + /// + public void SetPaletteFrom(IEnumerable bitmaps) + { + Dictionary colors = CountColors(bitmaps); + if (colors.Count == 0) + { + return; + } + + ushort[] palette; + if (colors.Count <= PaletteCapacity) + { + palette = new ushort[PaletteCapacity]; + Array.Fill(palette, (ushort)0x8000); + colors.Keys.CopyTo(palette, 0); + } + else + { + palette = MedianCut(colors, PaletteCapacity); + } + + Array.Copy(palette, Palette, PaletteCapacity); + } + + private static unsafe Dictionary CountColors(IEnumerable bitmaps) + { + var counts = new Dictionary(); + foreach (Bitmap bit in bitmaps) + { + if (bit == null) + { + continue; + } + + BitmapData bd = bit.LockBits(new Rectangle(0, 0, bit.Width, bit.Height), ImageLockMode.ReadOnly, PixelFormat.Format16bppArgb1555); + try + { + for (int y = 0; y < bit.Height; ++y) + { + var line = (ushort*)((byte*)bd.Scan0 + (y * bd.Stride)); + for (int x = 0; x < bit.Width; ++x) + { + if ((line[x] & 0x8000) == 0) + { + continue; + } + + ushort color = FrameEdit.VisibleColor(line[x]); + counts.TryGetValue(color, out int count); + counts[color] = count + 1; + } + } + } + finally + { + bit.UnlockBits(bd); + } + } + + return counts; + } + + /// + /// Reduces a colour histogram to at most colours by weighted median cut. + /// Unused entries of the result are 0x8000. + /// + private static ushort[] MedianCut(Dictionary histogram, int maxColors) + { + var boxes = new List>> { new List>(histogram) }; + + while (boxes.Count < maxColors) + { + int best = -1; + int bestChannel = 0; + long bestScore = 0; + for (int i = 0; i < boxes.Count; ++i) + { + if (boxes[i].Count < 2) + { + continue; + } + + long pixels = 0; + var min = new[] { 31, 31, 31 }; + var max = new[] { 0, 0, 0 }; + foreach (KeyValuePair entry in boxes[i]) + { + pixels += entry.Value; + for (int c = 0; c < 3; ++c) + { + int value = Channel(entry.Key, c); + min[c] = Math.Min(min[c], value); + max[c] = Math.Max(max[c], value); + } + } + + for (int c = 0; c < 3; ++c) + { + long score = (max[c] - min[c]) * pixels; + if (score > bestScore) + { + bestScore = score; + best = i; + bestChannel = c; + } + } + } + + if (best < 0) + { + break; + } + + List> box = boxes[best]; + int channel = bestChannel; + box.Sort((a, b) => Channel(a.Key, channel).CompareTo(Channel(b.Key, channel))); + + long total = 0; + foreach (KeyValuePair entry in box) + { + total += entry.Value; + } + + int split = box.Count - 1; + long sum = 0; + for (int i = 0; i < box.Count - 1; ++i) + { + sum += box[i].Value; + if (sum * 2 >= total) + { + split = i + 1; + break; + } + } + + boxes[best] = box.GetRange(0, split); + boxes.Add(box.GetRange(split, box.Count - split)); + } + + var palette = new ushort[maxColors]; + Array.Fill(palette, (ushort)0x8000); + for (int i = 0; i < boxes.Count; ++i) + { + long total = 0; + long red = 0; + long green = 0; + long blue = 0; + foreach (KeyValuePair entry in boxes[i]) + { + total += entry.Value; + red += (long)Channel(entry.Key, 0) * entry.Value; + green += (long)Channel(entry.Key, 1) * entry.Value; + blue += (long)Channel(entry.Key, 2) * entry.Value; + } + + int r = (int)((red + (total / 2)) / total); + int g = (int)((green + (total / 2)) / total); + int b = (int)((blue + (total / 2)) / total); + palette[i] = FrameEdit.VisibleColor((ushort)(0x8000 | (r << 10) | (g << 5) | b)); + } + + return palette; + } + + // 0 = red, 1 = green, 2 = blue + private static int Channel(ushort color, int channel) + { + return (color >> (10 - (5 * channel))) & 0x1F; + } + public void RemoveFrame(int index) { if (Frames == null) @@ -768,6 +1076,7 @@ public unsafe FrameEdit(Bitmap bit, ushort[] palette, int centerX, int centerY) var line = (ushort*)bd.Scan0; int delta = bd.Stride >> 1; var tmp = new List(); + var indexCache = new Dictionary(); for (int y = 0; y < bit.Height; ++y, line += delta) { @@ -781,7 +1090,7 @@ public unsafe FrameEdit(Bitmap bit, ushort[] palette, int centerX, int centerY) for (i = x; i <= bit.Width; ++i) { // first pixel set - if (i < bit.Width && cur[i] != 0) + if (i < bit.Width && (cur[i] & 0x8000) != 0) { break; } @@ -796,7 +1105,7 @@ public unsafe FrameEdit(Bitmap bit, ushort[] palette, int centerX, int centerY) for (j = (i + 1); j < bit.Width; ++j) { // next non set pixel - if (cur[j] == 0) + if ((cur[j] & 0x8000) == 0) { break; } @@ -815,8 +1124,14 @@ public unsafe FrameEdit(Bitmap bit, ushort[] palette, int centerX, int centerY) raw.data = new byte[raw.run]; while (r < raw.run) { - ushort col = cur[r + i]; - raw.data[r++] = GetPaletteIndex(palette, col); + ushort col = VisibleColor(cur[r + i]); + if (!indexCache.TryGetValue(col, out byte index)) + { + index = NearestIndex(palette, col); + indexCache[col] = index; + } + + raw.data[r++] = index; } tmp.Add(raw); x = j + 1; @@ -841,17 +1156,51 @@ public void ChangeCenter(int x, int y) Center = new Point(x, y); } - private static byte GetPaletteIndex(IReadOnlyList palette, ushort col) + /// + /// Index of in the palette, or of the closest colour when it is missing. + /// Entries of 0x8000 are skipped: the client draws them transparent. + /// + internal static byte NearestIndex(IReadOnlyList palette, ushort col) { + int red = (col >> 10) & 0x1F; + int green = (col >> 5) & 0x1F; + int blue = col & 0x1F; + + int best = 0; + int bestDistance = int.MaxValue; for (int i = 0; i < palette.Count; i++) { - if (palette[i] == col) + ushort entry = palette[i]; + if (entry == col) { return (byte)i; } + + if (entry == 0x8000) + { + continue; + } + + int dr = ((entry >> 10) & 0x1F) - red; + int dg = ((entry >> 5) & 0x1F) - green; + int db = (entry & 0x1F) - blue; + int distance = (2 * dr * dr) + (4 * dg * dg) + (3 * db * db); + if (distance < bestDistance) + { + bestDistance = distance; + best = i; + } } - return 0; + return (byte)best; + } + + /// + /// 0x8000 (black) is stored as 0, which the client draws transparent; keep it visible as 0,0,8. + /// + internal static ushort VisibleColor(ushort color) + { + return color == 0x8000 ? (ushort)0x8001 : color; } public void Save(BinaryWriter bin) diff --git a/UoFiddler.Controls/Forms/AnimationEditForm.cs b/UoFiddler.Controls/Forms/AnimationEditForm.cs index 5fd5123f..0bebfb6e 100644 --- a/UoFiddler.Controls/Forms/AnimationEditForm.cs +++ b/UoFiddler.Controls/Forms/AnimationEditForm.cs @@ -10,12 +10,12 @@ ***************************************************************************/ using System; +using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Linq; using System.Windows.Forms; -using System.Windows.Media.Imaging; using Ultima; using UoFiddler.Controls.Classes; @@ -1373,12 +1373,8 @@ private void OnClickReplace(object sender, EventArgs e) using (var bmpTemp = new Bitmap(dialog.FileName)) { - Bitmap bitmap = new Bitmap(bmpTemp); - - if (dialog.FileName.Contains(".bmp")) - { - bitmap = ConvertBmpAnim(bitmap, (int)numericUpDownRed.Value, (int)numericUpDownGreen.Value, (int)numericUpDownBlue.Value); - } + // Same background removal as Add, for every format + Bitmap bitmap = ConvertBmpAnim(bmpTemp, (int)numericUpDownRed.Value, (int)numericUpDownGreen.Value, (int)numericUpDownBlue.Value); AnimIdx edit = AnimationEdit.GetAnimation(_fileType, _currentBody, _currentAction, _currentDir); if (edit == null) @@ -1447,7 +1443,6 @@ private void OnClickAdd(object sender, EventArgs e) Bitmap[] bitBmp = new Bitmap[frameCount]; bitmap.SelectActiveFrame(dimension, 0); - UpdateGifPalette(bitmap, edit); ProgressBar.Maximum = frameCount; @@ -1523,12 +1518,18 @@ private void OnClickAdd(object sender, EventArgs e) private void AddImageAtCertainIndex(int frameCount, Bitmap[] bitBmp, Bitmap bmp, FrameDimension dimension, AnimIdx edit) { - // Return an Image at a certain index for (int index = 0; index < frameCount; index++) { bmp.SelectActiveFrame(dimension, index); bitBmp[index] = ConvertBmpAnim(bmp, (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, (int) numericUpDownBlue.Value); + } + + // One palette for all frames, not just the first + edit.FitPalette(bitBmp); + + for (int index = 0; index < frameCount; index++) + { edit.AddFrame(bitBmp[index], bitBmp[index].Width / 2); TreeNode node = GetNode(_currentBody); if (node != null) @@ -2047,8 +2048,6 @@ private void AddFilesAllDirections(OpenFileDialog dialog) bitmap.SelectActiveFrame(dimension, 0); - UpdateGifPalette(bitmap, edit); - Bitmap[] bitBmp = new Bitmap[frameCount]; AddImageAtCertainIndex(frameCount, bitBmp, bitmap, dimension, edit); @@ -2297,7 +2296,6 @@ private void AddAnimationX1(Color customConvert, Bitmap bmp) int frameCount = bmp.GetFrameCount(dimension); ProgressBar.Maximum = frameCount; bmp.SelectActiveFrame(dimension, 0); - UpdateGifPalette(bmp, edit); // Return an Image at a certain index Bitmap[] bitBmp = new Bitmap[frameCount]; @@ -2607,11 +2605,10 @@ private void AddAnimationX1(Color customConvert, Bitmap bmp) // End of Canvas algorithm + ConvertAndFitFrames(edit, bitBmp, 0, frameCount, dimension, ConvertBmpAnim); + for (int index = 0; index < frameCount; index++) { - bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnim(bitBmp[index], (int) numericUpDownRed.Value, - (int) numericUpDownGreen.Value, (int) numericUpDownBlue.Value); edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); TreeNode node = GetNode(_currentBody); if (node != null) @@ -2717,20 +2714,37 @@ private void OnClickGeneratePalette(object sender, EventArgs e) return; } - foreach (string filename in dialog.FileNames) + AnimIdx edit = AnimationEdit.GetAnimation(_fileType, _currentBody, _currentAction, _currentDir); + if (edit == null) { - Bitmap bit = new Bitmap(filename); - AnimIdx edit = AnimationEdit.GetAnimation(_fileType, _currentBody, _currentAction, _currentDir); - if (edit != null) + return; + } + + // One palette from all chosen images + var images = new List(dialog.FileNames.Length); + try + { + foreach (string filename in dialog.FileNames) { - bit = ConvertBmpAnim(bit, (int)numericUpDownRed.Value, (int)numericUpDownGreen.Value, (int)numericUpDownBlue.Value); - UpdateImagePalette(bit, edit); + using (var bit = new Bitmap(filename)) + { + images.Add(ConvertBmpAnim(bit, (int)numericUpDownRed.Value, (int)numericUpDownGreen.Value, (int)numericUpDownBlue.Value)); + } + } + + edit.SetPaletteFrom(images); + } + finally + { + foreach (Bitmap image in images) + { + image.Dispose(); } - SetPaletteBox(); - FramesListView.Invalidate(); - Options.ChangedUltimaClass["Animations"] = true; - SetPaletteBox(); } + + SetPaletteBox(); + FramesListView.Invalidate(); + Options.ChangedUltimaClass["Animations"] = true; } } //End of Soulblighter Modification @@ -2967,8 +2981,6 @@ private void AddDirectionsAddWithCanvasUniqueImageToolStripMenuItem_Click(object bitmap.SelectActiveFrame(dimension, 0); - UpdateGifPalette(bitmap, edit); - Bitmap[] bitBmp = new Bitmap[frameCount]; // Return an Image at a certain index @@ -3016,11 +3028,10 @@ private void AddDirectionsAddWithCanvasUniqueImageToolStripMenuItem_Click(object private AnimIdx Cv5AnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimension dimension, AnimIdx edit, Bitmap bmp) { // position 0 + ConvertAndFitFrames(edit, bitBmp, frameCount / 8 * 4, frameCount / 8 * 5, dimension, ConvertBmpAnimCv5); + for (int index = frameCount / 8 * 4; index < frameCount / 8 * 5; index++) { - bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnimCv5(bitBmp[index], (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, - (int) numericUpDownBlue.Value); edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); TreeNode node = GetNode(_currentBody); if (node != null) @@ -3064,13 +3075,10 @@ private AnimIdx Cv5AnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimens // position 1 edit = AnimationEdit.GetAnimation(_fileType, _currentBody, _currentAction, _currentDir); - bmp.SelectActiveFrame(dimension, 0); - UpdateGifPalette(bmp, edit); + ConvertAndFitFrames(edit, bitBmp, 0, frameCount / 8, dimension, ConvertBmpAnimCv5); + for (int index = 0; index < frameCount / 8; index++) { - bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnimCv5(bitBmp[index], (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, - (int) numericUpDownBlue.Value); edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); TreeNode node = GetNode(_currentBody); if (node != null) @@ -3114,13 +3122,10 @@ private AnimIdx Cv5AnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimens // position 2 edit = AnimationEdit.GetAnimation(_fileType, _currentBody, _currentAction, _currentDir); - bmp.SelectActiveFrame(dimension, 0); - UpdateGifPalette(bmp, edit); + ConvertAndFitFrames(edit, bitBmp, frameCount / 8 * 5, frameCount / 8 * 6, dimension, ConvertBmpAnimCv5); + for (int index = frameCount / 8 * 5; index < frameCount / 8 * 6; index++) { - bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnimCv5(bitBmp[index], (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, - (int) numericUpDownBlue.Value); edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); TreeNode node = GetNode(_currentBody); if (node != null) @@ -3164,13 +3169,10 @@ private AnimIdx Cv5AnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimens // position 3 edit = AnimationEdit.GetAnimation(_fileType, _currentBody, _currentAction, _currentDir); - bmp.SelectActiveFrame(dimension, 0); - UpdateGifPalette(bmp, edit); + ConvertAndFitFrames(edit, bitBmp, frameCount / 8 * 1, frameCount / 8 * 2, dimension, ConvertBmpAnimCv5); + for (int index = frameCount / 8 * 1; index < frameCount / 8 * 2; index++) { - bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnimCv5(bitBmp[index], (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, - (int) numericUpDownBlue.Value); edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); TreeNode node = GetNode(_currentBody); if (node != null) @@ -3214,13 +3216,10 @@ private AnimIdx Cv5AnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimens // position 4 edit = AnimationEdit.GetAnimation(_fileType, _currentBody, _currentAction, _currentDir); - bmp.SelectActiveFrame(dimension, 0); - UpdateGifPalette(bmp, edit); + ConvertAndFitFrames(edit, bitBmp, frameCount / 8 * 6, frameCount / 8 * 7, dimension, ConvertBmpAnimCv5); + for (int index = frameCount / 8 * 6; index < frameCount / 8 * 7; index++) { - bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnimCv5(bitBmp[index], (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, - (int) numericUpDownBlue.Value); edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); TreeNode node = GetNode(_currentBody); if (node != null) @@ -3668,8 +3667,6 @@ private void AllDirectionsAddWithCanvasKRFrameEditorColorCorrectorToolStripMenuI bitmap.SelectActiveFrame(dimension, 0); - UpdateGifPalette(bitmap, edit); - Bitmap[] bitBmp = new Bitmap[frameCount]; // Return an Image at a certain index @@ -3717,11 +3714,10 @@ private void AllDirectionsAddWithCanvasKRFrameEditorColorCorrectorToolStripMenuI private AnimIdx KrAnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimension dimension, AnimIdx edit, Bitmap bmp) { // position 0 + ConvertAndFitFrames(edit, bitBmp, frameCount / 5 * 0, frameCount / 5 * 1, dimension, ConvertBmpAnimKr); + for (int index = frameCount / 5 * 0; index < frameCount / 5 * 1; index++) { - bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnimKr(bitBmp[index], (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, - (int) numericUpDownBlue.Value); edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); TreeNode node = GetNode(_currentBody); if (node != null) @@ -3765,13 +3761,10 @@ private AnimIdx KrAnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimensi // position 1 edit = AnimationEdit.GetAnimation(_fileType, _currentBody, _currentAction, _currentDir); - bmp.SelectActiveFrame(dimension, 0); - UpdateGifPalette(bmp, edit); + ConvertAndFitFrames(edit, bitBmp, frameCount / 5 * 1, frameCount / 5 * 2, dimension, ConvertBmpAnimKr); + for (int index = frameCount / 5 * 1; index < frameCount / 5 * 2; index++) { - bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnimKr(bitBmp[index], (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, - (int) numericUpDownBlue.Value); edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); TreeNode node = GetNode(_currentBody); if (node != null) @@ -3815,13 +3808,10 @@ private AnimIdx KrAnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimensi // position 2 edit = AnimationEdit.GetAnimation(_fileType, _currentBody, _currentAction, _currentDir); - bmp.SelectActiveFrame(dimension, 0); - UpdateGifPalette(bmp, edit); + ConvertAndFitFrames(edit, bitBmp, frameCount / 5 * 2, frameCount / 5 * 3, dimension, ConvertBmpAnimKr); + for (int index = frameCount / 5 * 2; index < frameCount / 5 * 3; index++) { - bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnimKr(bitBmp[index], (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, - (int) numericUpDownBlue.Value); edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); TreeNode node = GetNode(_currentBody); if (node != null) @@ -3865,13 +3855,10 @@ private AnimIdx KrAnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimensi // position 3 edit = AnimationEdit.GetAnimation(_fileType, _currentBody, _currentAction, _currentDir); - bmp.SelectActiveFrame(dimension, 0); - UpdateGifPalette(bmp, edit); + ConvertAndFitFrames(edit, bitBmp, frameCount / 5 * 3, frameCount / 5 * 4, dimension, ConvertBmpAnimKr); + for (int index = frameCount / 5 * 3; index < frameCount / 5 * 4; index++) { - bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnimKr(bitBmp[index], (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, - (int) numericUpDownBlue.Value); edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); TreeNode node = GetNode(_currentBody); if (node != null) @@ -3915,13 +3902,10 @@ private AnimIdx KrAnimIdxPositions(int frameCount, Bitmap[] bitBmp, FrameDimensi // position 4 edit = AnimationEdit.GetAnimation(_fileType, _currentBody, _currentAction, _currentDir); - bmp.SelectActiveFrame(dimension, 0); - UpdateGifPalette(bmp, edit); + ConvertAndFitFrames(edit, bitBmp, frameCount / 5 * 4, frameCount / 5 * 5, dimension, ConvertBmpAnimKr); + for (int index = frameCount / 5 * 4; index < frameCount / 5 * 5; index++) { - bitBmp[index].SelectActiveFrame(dimension, index); - bitBmp[index] = ConvertBmpAnimKr(bitBmp[index], (int) numericUpDownRed.Value, (int) numericUpDownGreen.Value, - (int) numericUpDownBlue.Value); edit.AddFrame(bitBmp[index], bitBmp[index].Width/2); TreeNode node = GetNode(_currentBody); if (node != null) @@ -4428,128 +4412,50 @@ private void ConvertAndSetPalette(int selector) } } + /// + /// Sets the palette from the colours of every frame of a GIF, not just the first. + /// public void UpdateGifPalette(Bitmap bit, AnimIdx animIdx) { - using (MemoryStream imageStreamSource = new MemoryStream()) + FrameDimension dimension = new FrameDimension(bit.FrameDimensionsList[0]); + int frameCount = bit.GetFrameCount(dimension); + var frames = new Bitmap[frameCount]; + try { - ImageConverter ic = new ImageConverter(); - byte[] btImage = (byte[])ic.ConvertTo(bit, typeof(byte[])); - imageStreamSource.Write(btImage, 0, btImage.Length); - GifBitmapDecoder decoder = new GifBitmapDecoder(imageStreamSource, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default); - BitmapPalette pal = decoder.Palette; - int i; - for (i = 0; i < Animations.PaletteCapacity; i++) + for (int index = 0; index < frameCount; index++) { - animIdx.Palette[i] = 0; + bit.SelectActiveFrame(dimension, index); + frames[index] = ConvertBmpAnim(bit, (int)numericUpDownRed.Value, (int)numericUpDownGreen.Value, (int)numericUpDownBlue.Value); } - try - { - i = 0; - while (i < Animations.PaletteCapacity) //&& i < pal.Colors.Count) - { - int red = pal.Colors[i].R / 8; - int green = pal.Colors[i].G / 8; - int blue = pal.Colors[i].B / 8; - - int contaFinal = (0x400 * red) + (0x20 * green) + blue + 0x8000; - if (contaFinal == 0x8000) - { - contaFinal = 0x8001; - } - - animIdx.Palette[i] = (ushort)contaFinal; - i++; - } - } - catch (IndexOutOfRangeException) - { - // TODO: ignored? - } - catch (ArgumentOutOfRangeException) + animIdx.SetPaletteFrom(frames); + } + finally + { + foreach (Bitmap frame in frames) { - // TODO: ignored? + frame?.Dispose(); } - for (i = 0; i < Animations.PaletteCapacity; i++) - { - if (animIdx.Palette[i] < 0x8000) - { - animIdx.Palette[i] = 0x8000; - } - } + bit.SelectActiveFrame(dimension, 0); } } - public unsafe void UpdateImagePalette(Bitmap bit, AnimIdx animIdx) + /// + /// Converts frames [start, end) in place, then fits the palette of to all of them + /// before they are added. + /// + private void ConvertAndFitFrames(AnimIdx edit, Bitmap[] bitBmp, int start, int end, FrameDimension dimension, + Func convert) { - int count = 0; - var bmp = new Bitmap(bit); - BitmapData bd = bmp.LockBits( - new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format16bppArgb1555); - var line = (ushort*)bd.Scan0; - int delta = bd.Stride >> 1; - - int i = 0; - while (i < Animations.PaletteCapacity) + for (int index = start; index < end; index++) { - animIdx.Palette[i] = 0; - i++; + bitBmp[index].SelectActiveFrame(dimension, index); + bitBmp[index] = convert(bitBmp[index], (int)numericUpDownRed.Value, (int)numericUpDownGreen.Value, + (int)numericUpDownBlue.Value); } - int y = 0; - - while (y < bmp.Height) - { - ushort* cur = line; - for (int x = 0; x < bmp.Width; x++) - { - ushort c = cur[x]; - if (c == 0) - { - continue; - } - - bool found = false; - i = 0; - - while (i < animIdx.Palette.Length) - { - if (animIdx.Palette[i] == c) - { - found = true; - break; - } - i++; - } - - if (!found) - { - animIdx.Palette[count++] = c; - } - - if (count >= Animations.PaletteCapacity) - { - break; - } - } - - for (i = 0; i < Animations.PaletteCapacity; i++) - { - if (animIdx.Palette[i] < 0x8000) - { - animIdx.Palette[i] = 0x8000; - } - } - - if (count >= Animations.PaletteCapacity) - { - break; - } - - y++; - line += delta; - } + edit.FitPalette(bitBmp[start..end]); } public void PaletteConverter(int selector, AnimIdx animIdx) From be19fc85fd1fe5af3b0a2d1bce15f39c10a80f46 Mon Sep 17 00:00:00 2001 From: AsY!um- <377468+AsYlum-@users.noreply.github.com> Date: Thu, 12 Nov 2026 01:29:49 +0100 Subject: [PATCH 4/5] Export Bmp and Jpg animation frames on white Without transparency the background came out black and re-imported as a black box; import removes white. --- UoFiddler.Controls/Forms/AnimationEditForm.cs | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/UoFiddler.Controls/Forms/AnimationEditForm.cs b/UoFiddler.Controls/Forms/AnimationEditForm.cs index 0bebfb6e..cf500394 100644 --- a/UoFiddler.Controls/Forms/AnimationEditForm.cs +++ b/UoFiddler.Controls/Forms/AnimationEditForm.cs @@ -1181,12 +1181,7 @@ private void OnClickExtractImages(object sender, EventArgs e) } string filename = string.Format("anim{5}_{0}_{1}_{2}_{3}{4}", body, a, i, j, menu.Tag, _fileType); - string file = Path.Combine(path, filename); - - using (Bitmap bit = new Bitmap(bits[j])) - { - bit.Save(file, format); - } + SaveFrameImage(bits[j], Path.Combine(path, filename), format); } } } @@ -1210,12 +1205,7 @@ private void OnClickExtractImages(object sender, EventArgs e) } string filename = string.Format("anim{5}_{0}_{1}_{2}_{3}{4}", body, action, i, j, menu.Tag, _fileType); - string file = Path.Combine(path, filename); - - using (Bitmap bit = new Bitmap(bits[j])) - { - bit.Save(file, format); - } + SaveFrameImage(bits[j], Path.Combine(path, filename), format); } } } @@ -1223,6 +1213,34 @@ private void OnClickExtractImages(object sender, EventArgs e) FileSavedDialog.Show(FindForm(), path, "Frames saved successfully."); } + /// + /// Saves a frame. Bmp and Jpg have no transparency, so they get a white background, which import + /// removes again; saved as is, the background turned black and came back as a black box. + /// + private static void SaveFrameImage(Bitmap frame, string file, ImageFormat format) + { + if (!format.Equals(ImageFormat.Bmp) && !format.Equals(ImageFormat.Jpeg)) + { + using (var bit = new Bitmap(frame)) + { + bit.Save(file, format); + } + + return; + } + + using (var bit = new Bitmap(frame.Width, frame.Height, PixelFormat.Format24bppRgb)) + { + using (Graphics g = Graphics.FromImage(bit)) + { + g.Clear(Color.White); + g.DrawImage(frame, 0, 0, frame.Width, frame.Height); + } + + bit.Save(file, format); + } + } + private void OnClickRemoveAction(object sender, EventArgs e) { if (_fileType == 0) From 69abb8780730b3806370229d35740b3afbefc1a5 Mon Sep 17 00:00:00 2001 From: AsY!um- <377468+AsYlum-@users.noreply.github.com> Date: Thu, 12 Nov 2026 01:29:54 +0100 Subject: [PATCH 5/5] Replace an action from its exported images in All directions add (#28) Images named ..._{direction}_{frame} are sorted into directions and frames; each frame keeps the center of the frame it replaces. Five GIFs work as before. --- .../Forms/AnimationEditForm.Designer.cs | 2 +- UoFiddler.Controls/Forms/AnimationEditForm.cs | 165 ++++++++++++++---- 2 files changed, 132 insertions(+), 35 deletions(-) diff --git a/UoFiddler.Controls/Forms/AnimationEditForm.Designer.cs b/UoFiddler.Controls/Forms/AnimationEditForm.Designer.cs index b0a1642f..32485289 100644 --- a/UoFiddler.Controls/Forms/AnimationEditForm.Designer.cs +++ b/UoFiddler.Controls/Forms/AnimationEditForm.Designer.cs @@ -511,7 +511,7 @@ private void InitializeComponent() allDirectionsAddToolStripMenuItem.Name = "allDirectionsAddToolStripMenuItem"; allDirectionsAddToolStripMenuItem.Size = new System.Drawing.Size(412, 22); allDirectionsAddToolStripMenuItem.Text = "All directions add"; - allDirectionsAddToolStripMenuItem.ToolTipText = "Select 5 Gifs to add ,one by one, in a diferent direction "; + allDirectionsAddToolStripMenuItem.ToolTipText = "Select 5 GIFs to add, one per direction, or select an action's images named ..._{direction}_{frame} (as Extract Images writes them) to replace its frames"; allDirectionsAddToolStripMenuItem.Click += AllDirectionsAddToolStripMenuItem_Click; // // allDirectionsAddWithCanvasToolStripMenuItem diff --git a/UoFiddler.Controls/Forms/AnimationEditForm.cs b/UoFiddler.Controls/Forms/AnimationEditForm.cs index cf500394..738dd5e2 100644 --- a/UoFiddler.Controls/Forms/AnimationEditForm.cs +++ b/UoFiddler.Controls/Forms/AnimationEditForm.cs @@ -1988,54 +1988,151 @@ private void ToolStripLockButton_Click(object sender, EventArgs e) // Add in all Directions private void AllDirectionsAddToolStripMenuItem_Click(object sender, EventArgs e) { - if (_fileType != 0) + if (_fileType == 0) { - using (OpenFileDialog dialog = new OpenFileDialog()) + return; + } + + using (OpenFileDialog dialog = new OpenFileDialog()) + { + dialog.Multiselect = true; + dialog.Title = "Choose 5 GIFs (one per direction), or the images of an action named ..._{direction}_{frame}"; + dialog.CheckFileExists = true; + dialog.Filter = "Gif or image files|*.gif;*.bmp;*.png;*.tif;*.tiff;*.jpg;*.jpeg|Gif files (*.gif)|*.gif|Image files|*.bmp;*.png;*.tif;*.tiff;*.jpg;*.jpeg"; + if (dialog.ShowDialog() != DialogResult.OK) { - dialog.Multiselect = true; - dialog.Title = "Choose 5 GIFs to add"; - dialog.CheckFileExists = true; - dialog.Filter = "Gif files (*.gif;)|*.gif;"; - if (dialog.ShowDialog() == DialogResult.OK) + return; + } + + string[] files = dialog.FileNames; + int gifs = files.Count(file => Path.GetExtension(file).Equals(".gif", StringComparison.OrdinalIgnoreCase)); + if (gifs == files.Length) + { + if (files.Length != 5) { - DirectionTrackBar.Enabled = false; - if (dialog.FileNames.Length == 5) - { - DirectionTrackBar.Value = 0; + MessageBox.Show("Choose exactly 5 GIFs, one per direction.", "All Directions Add", + MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } - AddFilesAllDirections(dialog); - } + DirectionTrackBar.Enabled = false; + DirectionTrackBar.Value = 0; + AddFilesAllDirections(dialog); + DirectionTrackBar.Enabled = true; + } + else if (gifs > 0) + { + MessageBox.Show("Choose either 5 GIFs or single images, not both.", "All Directions Add", + MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + else if (!ReplaceActionFromImages(files)) + { + return; + } + } - // Looping if dialog.FileNames.Length != 5 - while (dialog.FileNames.Length != 5) - { - if (dialog.ShowDialog() == DialogResult.Cancel) - { - break; - } + // Refresh List + _currentDir = DirectionTrackBar.Value; + AfterSelectTreeView(null, null); + } - if (dialog.FileNames.Length != 5) - { - dialog.ShowDialog(); - } + // Matches the names Extract Images writes: anim{file}_{body}_{action}_{direction}_{frame} + private static readonly System.Text.RegularExpressions.Regex _directionFrameName = + new System.Text.RegularExpressions.Regex(@"_(\d+)_(\d+)$"); - if (dialog.FileNames.Length != 5) - { - continue; - } + /// + /// Replaces the frames of the current action with single images, sorted into directions and frames + /// by their names (..._{direction}_{frame}). Frame N keeps the center frame N had; extra frames get + /// the bottom middle of the image. Returns false when nothing was changed. + /// + private bool ReplaceActionFromImages(string[] files) + { + var entries = new List<(int Direction, int Frame, string File)>(); + var invalid = new List(); + foreach (string file in files) + { + var match = _directionFrameName.Match(Path.GetFileNameWithoutExtension(file)); + if (!match.Success || !int.TryParse(match.Groups[1].Value, out int direction) || direction > 4 || + !int.TryParse(match.Groups[2].Value, out int frame)) + { + invalid.Add(Path.GetFileName(file)); + continue; + } - DirectionTrackBar.Value = 0; + entries.Add((direction, frame, file)); + } + + invalid.AddRange(entries.GroupBy(entry => (entry.Direction, entry.Frame)) + .Where(group => group.Count() > 1) + .SelectMany(group => group.Select(entry => Path.GetFileName(entry.File)))); - AddFilesAllDirections(dialog); + if (invalid.Count > 0) + { + string list = string.Join(Environment.NewLine, invalid.Take(10)) + (invalid.Count > 10 ? $"{Environment.NewLine}..." : string.Empty); + MessageBox.Show($"Image names must end in _{{direction}}_{{frame}} (direction 0-4), once per direction and frame, " + + $"as Extract Images writes them. Not usable:{Environment.NewLine}{list}", "All Directions Add", + MessageBoxButtons.OK, MessageBoxIcon.Information); + return false; + } + + var directions = entries.GroupBy(entry => entry.Direction).OrderBy(group => group.Key).ToList(); + string directionList = string.Join(", ", directions.Select(group => group.Key)); + if (MessageBox.Show($"Replace the frames of body {_currentBody}, action {_currentAction} in direction(s) {directionList} " + + $"with {entries.Count} images?", "All Directions Add", MessageBoxButtons.YesNo, MessageBoxIcon.Question, + MessageBoxDefaultButton.Button2) != DialogResult.Yes) + { + return false; + } + + foreach (var direction in directions) + { + AnimIdx edit = AnimationEdit.GetAnimation(_fileType, _currentBody, _currentAction, direction.Key); + if (edit == null) + { + continue; + } + + List centers = edit.Frames?.Select(frame => frame.Center).ToList() ?? new List(); + + var bitmaps = new List(); + try + { + foreach (var entry in direction.OrderBy(entry => entry.Frame)) + { + using (var image = new Bitmap(entry.File)) + { + bitmaps.Add(ConvertBmpAnim(image, (int)numericUpDownRed.Value, (int)numericUpDownGreen.Value, (int)numericUpDownBlue.Value)); } - DirectionTrackBar.Enabled = true; + } + + edit.ClearFrames(); + // One palette for the whole direction + edit.FitPalette(bitmaps); + for (int i = 0; i < bitmaps.Count; ++i) + { + Point center = i < centers.Count ? centers[i] : new Point(bitmaps[i].Width / 2, 0); + edit.AddFrame(bitmaps[i], center.X, center.Y); + } + } + finally + { + foreach (Bitmap bitmap in bitmaps) + { + bitmap.Dispose(); } } } - // Refresh List - _currentDir = DirectionTrackBar.Value; - AfterSelectTreeView(null, null); + TreeNode node = GetNode(_currentBody); + if (node != null) + { + node.ForeColor = Color.Empty; + node.Nodes[_currentAction].ForeColor = Color.Empty; + } + + Options.ChangedUltimaClass["Animations"] = true; + return true; } private void AddFilesAllDirections(OpenFileDialog dialog)