A tool to index convert files into 4bpp indexed graphics with palettes to be used with embedded development kits.

BitmapProcessor.cs 7.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Threading.Tasks;
  4. using System.Drawing;
  5. using System.Drawing.Imaging;
  6. using System.IO;
  7. using System.Linq;
  8. namespace agbidx.ImageLib
  9. {
  10. public static class BitmapProcessor
  11. {
  12. public static async Task ProcessPokemonImage(string outDirectory, string inPath, bool verbose, bool scanDirectories, bool recursive)
  13. {
  14. //Create the original bitmap
  15. if(Directory.Exists(inPath))
  16. {
  17. if(scanDirectories)
  18. {
  19. List<Task> subTasks = new List<Task>();
  20. foreach(string s in Directory.GetFiles(inPath))
  21. {
  22. subTasks.Add(ProcessPokemonImage(outDirectory, s, verbose, recursive, recursive));
  23. }
  24. foreach(string s in Directory.GetDirectories(inPath))
  25. {
  26. subTasks.Add(ProcessPokemonImage(outDirectory, s, verbose, recursive, recursive));
  27. }
  28. await Task.WhenAll(subTasks);
  29. }
  30. }
  31. else
  32. {
  33. await Task.Run(() => {
  34. Bitmap input = new Bitmap(inPath);
  35. ProcessPokemonImageFront(input, outDirectory, Path.GetFileNameWithoutExtension(inPath), verbose);
  36. });
  37. }
  38. }
  39. private static Color[] Quantize4bppColors(Bitmap bmp)
  40. {
  41. List<Color> colors = new List<Color>(16);
  42. BitmapData bitmapData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);
  43. int bytesPerPixel = System.Drawing.Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;
  44. int heightInPixels = bitmapData.Height;
  45. int widthInBytes = bitmapData.Width * bytesPerPixel;
  46. int palCount = 0;
  47. unsafe
  48. {
  49. byte* ptrFirstPixel = (byte*)bitmapData.Scan0;
  50. for (int y = 0; y < heightInPixels; y++)
  51. {
  52. byte* currentLine = ptrFirstPixel + (y * bitmapData.Stride);
  53. for (int x = 0; x < widthInBytes; x = x + bytesPerPixel)
  54. {
  55. int blue = currentLine[x];
  56. int green = currentLine[x + 1];
  57. int red = currentLine[x + 2];
  58. if (!colors.Any(c => c.R == red && c.G == green && c.B == blue))
  59. {
  60. colors.Add(Color.FromArgb(red,green,blue));
  61. palCount++;
  62. if(palCount > 16)
  63. throw new ArgumentException("The image has too many colors", "bmp");
  64. }
  65. }
  66. }
  67. }
  68. bmp.UnlockBits(bitmapData);
  69. return colors.ToArray();
  70. }
  71. private static Bitmap RenderBitmapFromPalette(Bitmap original, Color[] palette)
  72. {
  73. Bitmap output = new Bitmap(original.Width,original.Height, PixelFormat.Format4bppIndexed);
  74. BitmapData targetData = output.LockBits(new Rectangle(0, 0, original.Width,original.Height),
  75. ImageLockMode.ReadWrite, PixelFormat.Format4bppIndexed);
  76. BitmapData sourceData = original.LockBits(new Rectangle(0, 0, original.Width,original.Height),
  77. ImageLockMode.ReadOnly, original.PixelFormat);
  78. int bytesPerPixel = System.Drawing.Bitmap.GetPixelFormatSize(original.PixelFormat) / 8;
  79. int heightInPixels = sourceData.Height;
  80. int widthInBytes = sourceData.Width * bytesPerPixel;
  81. unsafe
  82. {
  83. byte* ptrFirstPixel = (byte*)sourceData.Scan0;
  84. for (int y = 0; y < heightInPixels; y++)
  85. {
  86. byte* currentLine = ptrFirstPixel + (y * sourceData.Stride);
  87. byte* pTarget = (byte*) (targetData.Scan0 + y*targetData.Stride);
  88. byte prevValue = 0;
  89. for (int x = 0; x < widthInBytes; x = x + bytesPerPixel)
  90. {
  91. int blue = currentLine[x];
  92. int green = currentLine[x + 1];
  93. int red = currentLine[x + 2];
  94. if (!palette.Any(c => c.R == red && c.G == green && c.B == blue))
  95. throw new ArgumentException($"The color palette does not contain the required entry {red}:{green}:{blue}.","palette");
  96. else
  97. {
  98. byte colorIndex = (byte)Array.IndexOf(palette, Color.FromArgb(red,green,blue));
  99. if((x/bytesPerPixel) % 2 == 0)
  100. prevValue = colorIndex;
  101. else
  102. {
  103. *pTarget = (byte)((prevValue<<4) | (colorIndex));
  104. pTarget++;
  105. }
  106. }
  107. }
  108. }
  109. }
  110. output.UnlockBits(targetData);
  111. original.UnlockBits(sourceData);
  112. ColorPalette pal = output.Palette;
  113. for(int i = 0; i < ((palette.Length > 16) ? 16 : palette.Length); ++i)
  114. {
  115. pal.Entries[i] = palette[i];
  116. }
  117. output.Palette = pal;
  118. return output;
  119. }
  120. private static void ProcessPokemonImageFront(Bitmap inFile, string outDirectory, string name, bool verbose)
  121. {
  122. Bitmap normal = new Bitmap(128,64, PixelFormat.Format32bppRgb);
  123. Bitmap shiny = new Bitmap(128,64, PixelFormat.Format32bppRgb);
  124. using(Graphics g = Graphics.FromImage(normal))
  125. {
  126. g.DrawImage(inFile,new Rectangle(0,0,64,64),new Rectangle(0,0,64,64), GraphicsUnit.Pixel);
  127. g.DrawImage(inFile,new Rectangle(64,0,64,64),new Rectangle(2*64,0,64,64), GraphicsUnit.Pixel);
  128. }
  129. using(Graphics g = Graphics.FromImage(shiny))
  130. {
  131. g.DrawImage(inFile ,new Rectangle(0,0,64,64),new Rectangle(1*64,0,64,64), GraphicsUnit.Pixel);
  132. g.DrawImage(inFile ,new Rectangle(64,0,64,64),new Rectangle(3*64,0,64,64), GraphicsUnit.Pixel);
  133. }
  134. Color[] normalPalette = Quantize4bppColors(normal);
  135. Color[] shinyPalette = Quantize4bppColors(shiny);
  136. Bitmap normalIndexed = RenderBitmapFromPalette(normal, normalPalette);
  137. Bitmap shinyIndexed = RenderBitmapFromPalette(shiny, shinyPalette);
  138. Bitmap frontNormalIndexed = normalIndexed.Clone(new Rectangle(0,0,64,64), PixelFormat.Format4bppIndexed);
  139. Bitmap backShinyIndexed = shinyIndexed.Clone(new Rectangle(64,0,64,64), PixelFormat.Format4bppIndexed);
  140. frontNormalIndexed.Save(Path.Combine(outDirectory, name+"_frontnormal.png"),ImageFormat.Png);
  141. backShinyIndexed.Save(Path.Combine(outDirectory,name+"_backshiny.png"),ImageFormat.Png);
  142. if(verbose)
  143. {
  144. Console.WriteLine("Processed " + Path.Combine(outDirectory, name+"_frontnormal.png"));
  145. Console.WriteLine("Processed " + Path.Combine(outDirectory,name+"_backshiny.png"));
  146. }
  147. }
  148. }
  149. }