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)