diff --git a/LanternExtractor/Infrastructure/EqBmp.cs b/LanternExtractor/Infrastructure/EqBmp.cs new file mode 100644 index 0000000..cc043ae --- /dev/null +++ b/LanternExtractor/Infrastructure/EqBmp.cs @@ -0,0 +1,121 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; + +namespace LanternExtractor.Infrastructure +{ + /// + /// Wrapper over Bitmap with various hacks to make libgdiplus + /// consistently create transparent pngs on MacOS & Linux + /// + public class EqBmp + { + private static readonly bool _needsGdipHacks = Environment.OSVersion.Platform == PlatformID.MacOSX || + Environment.OSVersion.Platform == PlatformID.Unix; + private static bool _hasCheckedForPaletteFlagsField; + private static System.Reflection.FieldInfo _paletteFlagsField = null; + + public PixelFormat PixelFormat => _bitmap.PixelFormat; + + private readonly Bitmap _bitmap; + private readonly ColorPalette _palette; + + public EqBmp(Stream stream) + { + SetPaletteFlagsField(); + + _bitmap = new Bitmap(stream); + _palette = _bitmap.Palette; + } + + public void WritePng(string outputFilePath) + { + _bitmap.Save(outputFilePath, ImageFormat.Png); + } + + public void MakeMagentaTransparent() + { + _bitmap.MakeTransparent(Color.Magenta); + if (_needsGdipHacks) + { + // https://github.com/ mono/libgdiplus/commit/bf9a1440b7bfea704bf2cb771f5c2b5c09e7bcfa + _bitmap.MakeTransparent(Color.FromArgb(0, Color.Magenta)); + } + } + + public void MakePaletteTransparent(int transparentIndex) + { + if (_needsGdipHacks) + { + // https://github.com/mono/libgdiplus/issues/702 + _paletteFlagsField?.SetValue(_palette, _palette.Flags | (int)PaletteFlags.HasAlpha); + } + + var transparentColor = GetTransparentPaletteColor(); + _palette.Entries[transparentIndex] = transparentColor; + _bitmap.Palette = _palette; + + if (_needsGdipHacks) + { + // Due to a bug with the libgdiplus implementation of System.Drawing, setting a color palette + // entry to transparent does not work. The workaround is to ensure that the transparent + // key is unique and then use MakeTransparent() + _bitmap.MakeTransparent(transparentColor); + } + } + + private Color GetTransparentPaletteColor() + { + var transparencyColor = Color.FromArgb(0, 0, 0, 0); + + if (!_needsGdipHacks) + { + return transparencyColor; + } + + var random = new Random(); + var foundUnique = false; + + while (!foundUnique) + { + foundUnique = _palette.Entries.All(e => e != transparencyColor); + transparencyColor = Color.FromArgb(0, random.Next(256), random.Next(256), random.Next(256)); + } + + return transparencyColor; + } + + // https://github.com/Robmaister/SharpFont/blob/422bdab059dd8e594b4b061a3b53152e71342ce2/Source/SharpFont.GDI/FTBitmapExtensions.cs + // https://github.com/Robmaister/SharpFont/pull/136 + private static void SetPaletteFlagsField() + { + if (!_needsGdipHacks || _hasCheckedForPaletteFlagsField) + { + return; + } + + _hasCheckedForPaletteFlagsField = true; + + // The field needed may be named "flags" or "_flags", dependin on the version of Mono. To be thorough, check for the first Name that contains "lags". + var fields = typeof(ColorPalette).GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + + for (int i = 0; i < fields.Length; i++) + { + if (fields[i].Name.Contains("lags")) + { + _paletteFlagsField = fields[i]; + break; + } + } + } + + enum PaletteFlags + { + HasAlpha = 0x0001, + GrayScale = 0x0002, + HalfTone = 0x0004, + } + } +} diff --git a/LanternExtractor/Infrastructure/ImageWriter.cs b/LanternExtractor/Infrastructure/ImageWriter.cs index ac9471f..2625509 100644 --- a/LanternExtractor/Infrastructure/ImageWriter.cs +++ b/LanternExtractor/Infrastructure/ImageWriter.cs @@ -31,8 +31,6 @@ public static void WriteImageAsPng(byte[] bytes, string filePath, string fileNam private static void WriteBmpAsPng(byte[] bytes, string filePath, string fileName, bool isMasked, bool rotate, ILogger logger) { - var byteStream = new MemoryStream(bytes); - if (string.IsNullOrEmpty(filePath)) { return; @@ -40,11 +38,12 @@ private static void WriteBmpAsPng(byte[] bytes, string filePath, string fileName Directory.CreateDirectory(filePath); - Bitmap image; + EqBmp image; + var byteStream = new MemoryStream(bytes); try { - image = new Bitmap(byteStream); + image = new EqBmp(byteStream); } catch (Exception e) { @@ -60,76 +59,21 @@ private static void WriteBmpAsPng(byte[] bytes, string filePath, string fileName fileName = "canwall1.png"; } - Bitmap cloneBitmap; - - if (isMasked) + switch (image.PixelFormat) { - cloneBitmap = image.Clone(new Rectangle(0, 0, image.Width, image.Height), - PixelFormat.Format8bppIndexed); - - int paletteIndex = GetPaletteIndex(fileName); - var palette = cloneBitmap.Palette; - - if (Environment.OSVersion.Platform != PlatformID.MacOSX && - Environment.OSVersion.Platform != PlatformID.Unix) - { - palette.Entries[paletteIndex] = Color.FromArgb(0, 0, 0, 0); - cloneBitmap.Palette = palette; - } - else - { - // Due to a bug with the MacOS implementation of System.Drawing, setting a color palette value to - // transparent does not work. The workaround is to ensure that the first palette value (the transparent - // key) is unique and then use MakeTransparent() - Color transparencyColor = palette.Entries[paletteIndex]; - bool isUnique = false; - - while (!isUnique) - { - isUnique = true; - - for (var i = 1; i < cloneBitmap.Palette.Entries.Length; i++) - { - Color paletteValue = cloneBitmap.Palette.Entries[i]; - - if (paletteValue == transparencyColor) - { - Random random = new Random(); - transparencyColor = Color.FromArgb(random.Next(256), random.Next(256), random.Next(256)); - isUnique = false; - break; - } - } - } - - palette.Entries[paletteIndex] = transparencyColor; - cloneBitmap.Palette = palette; - cloneBitmap.MakeTransparent(transparencyColor); - - // For some reason, this now has to be done to ensure the pixels are actually set to transparent - // Another head scratching MacOS bug - for (int i = 0; i < cloneBitmap.Width; ++i) + case PixelFormat.Format8bppIndexed: + if (isMasked) { - for (int j = 0; j < cloneBitmap.Height; ++j) - { - if (cloneBitmap.GetPixel(i, j) == transparencyColor) - { - cloneBitmap.SetPixel(i, j, Color.FromArgb(0, 0, 0, 0)); - } - } + var paletteIndex = GetPaletteIndex(fileName); + image.MakePaletteTransparent(paletteIndex); } - } - } - else - { - cloneBitmap = image.Clone(new Rectangle(0, 0, image.Width, image.Height), PixelFormat.Format32bppArgb); - if (image.PixelFormat != PixelFormat.Format8bppIndexed) - { - cloneBitmap.MakeTransparent(Color.Magenta); - } + break; + default: + image.MakeMagentaTransparent(); + break; } - cloneBitmap.Save(Path.Combine(filePath, fileName), ImageFormat.Png); + image.WritePng(Path.Combine(filePath, fileName)); } private static void WriteDdsAsPng(byte[] bytes, string filePath, string fileName)