MakePaletteSwatch

From TheAlmightyGuru
Revision as of 10:58, 11 June 2021 by TheAlmightyGuru (talk | contribs) (Usage)
Jump to: navigation, search
Example output.

MakePaletteSwatch is an command line open source program I wrote in C# which extracts the palette from an image and generates a color swatch image of it.

The program supports several input formats including PNG, GIF, BMP, and ICO. It also supports indexed palettes with a color depth of 1, 4, or 8 bits-per-pixel (2, 16, or 256 colors). The resulting palette swatch image will have the same color depth and palette as the original, but will be a series of pixels matching every color index in the palette. You may customize the output file name and format and how many columns wide you want the palette image.

Personal

I always enjoy looking at swatches of the palettes used in older images, but I've never found a program that can easily export the palette into a nice swatch graphic. I tried making one using an ImagMagick script, but couldn't figure out how. Then, I wrote the program in FreeBASIC which worked, but I was unhappy that FreeBASIC only has native support for BMP format, and it doesn't have very good image manipulation for limited palettes either, so I decided to try writing it in C# where I had access to GDI. Although it was easier to write the program in C#, I discovered that GDI doesn't have an easy way to create bitmaps using an indexed palette, but I did eventually get it working. Also, while I'm happy with how the program functions, I don't like C#'s horrible bloat. My original FreeBASIC program was 150 KB and would run on all versions of Windows out of the box. The C# program is 660 KB and requires the .Net 5.0 64-bit runtimes. At some point in the future, I'd like to port this to C to have it be a single small EXE again. I'd also like to add a custom feature for the swatch size since the generated image isn't useful to most people.

Usage

The easiest way to run the program is to simply drag and drop an image over the MakePaletteSwatch exe. This will run the program with its default settings and generate an image of the color swatches with the name Palette.png. If the palette image isn't generated, you probably supplied the program with an invalid image, or don't have the .net runtimes installed.

You can also run the command from the command line with the following arguments:

MakePaletteSwatch.exe Input_Image_File [Output_Palette_File] [Swatch_Width]
  • Input_Image_File is the name of the file containing the palette you want to use.
  • Output_Palette_File [Optional] is the name of the desired color swatch image. The format is inferred from the extension.
  • Swatch_Width [Optional] is the number of colors wide you want the swatch image to be. If not supplied, an 8-bit palette will be 16 columns, a 4-bit palette will be 4 columns, and a 1-bit palette will be 2 column.

Download

  • Download (Info) - version 2021-02-01. Requires .Net v5.0 64-bit.

Source

Here is the source code for the program if you want to build it yourself.

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;

namespace MakePaletteSwatch
{
	class MakePeletteSwatch
	{
		static void Main(string[] args)
		{
			string inputImagePath = "", outputPalettePath = "Palette.png", extension;
			int swatchWidth = 0, swatchHeight;
			ImageFormat format = ImageFormat.Png;

			// Process the arguments.
			if(args.Length == 0)
			{
				// No arguments. Print the help.
				Console.WriteLine("usage: makepaletteswatch Input_Image_File [Output_Palette_File] [Swatch_Width]");
				return;
			}
			if(args.Length > 0)
			{
				// An input file name was supplied. Verify it exists.
				inputImagePath = args[0];
				if(!File.Exists(inputImagePath))
				{
					Console.WriteLine($"Input file: {inputImagePath} does not exist.");
					return;
				}
			}
			if(args.Length > 1)
			{
				// An palette swatch file name was supplied. Determine the format from the extension.
				outputPalettePath = args[1];

				extension = Path.GetExtension(outputPalettePath).ToLower();

				switch (extension)
				{
				case ".bmp": format = ImageFormat.Bmp; break;
				case ".gif": format = ImageFormat.Gif; break;
				case ".png": format = ImageFormat.Png; break;
				default:
					Console.WriteLine($"Must specify a valid palette extension (bmp, gif, or png). Current is: {extension.ToUpper()}.");
					return;
				}
			}
			if (args.Length > 2)
			{
				// A palette swatch width was supplied. Verify it.
				swatchWidth = Int32.Parse(args[2]);
				if (swatchWidth < 1 || swatchWidth > 256)
					swatchWidth = 0;
			}

			// Attempt to open the specified image.
			Bitmap bitmap;
			try
			{
				bitmap = new Bitmap(inputImagePath);
			}
			catch
			{
				Console.WriteLine($"Could not open {inputImagePath}. Image type not supported.");
				Console.WriteLine("Valid input formats are: BMP, GIF, ICO, or PNG.");
				return;
			}

			// Get details about the palette.
			int defaultWidth, paletteIndexSize;
			bool valid;
			switch(bitmap.PixelFormat)
			{
			case PixelFormat.Format1bppIndexed:
				paletteIndexSize = 2;
				defaultWidth = 2;		// Creates a 2x1 swatch.
				valid = true;
				break;
			case PixelFormat.Format4bppIndexed:
				paletteIndexSize = 16;
				defaultWidth = 4;		// Creates a 4x4 swatch.
				valid = true;
				break;
			case PixelFormat.Format8bppIndexed:
				paletteIndexSize = 256;
				defaultWidth = 16;		// Creates a 16x16 swatch.
				valid = true;
				break;
			default:
				paletteIndexSize = 0;
				defaultWidth = 0;
				valid = false;
				break;
			}

			// Verify the image uses an indexed palette.
			if (!valid)
			{
				Console.WriteLine($"Image {inputImagePath} does not use a 1, 4, or 8-bit palette.");
				return;
			}

			// Unless with swatch width has been overridden, get the width necessary to make a square swatch image.
			if (swatchWidth == 0)
				swatchWidth = defaultWidth;

			// The swatch's necessary height is derived from the desired width and palette index size.
			double height = ((double)paletteIndexSize / swatchWidth);
			swatchHeight = (int)Math.Ceiling(height);

			// Create the palette swatch image. This is where we will draw one of each color of the palette.
			Bitmap swatch = new Bitmap(swatchWidth, swatchHeight, bitmap.PixelFormat);

			// Copy the palette from the input image to the palette swatch image.
			swatch.Palette = bitmap.Palette;

			// This loop with draw every palette index in the swatch image.
			for(int y = 0; y < swatchHeight; y++)
			{
				for (int x = 0; x < swatchWidth; x++)
				{
					// Lock the current pixel.
					// It's possible to do this with only locking the bitmap data once and updating the entire image, 
					// but it requires a bunch of ugly bishifting. Since the image will never be larger than 256 pixels, 
					// I'm doing it the slow way, but it's a lot easier to read.
					BitmapData bmpData = swatch.LockBits(new Rectangle(new Point(x, y), new Size(1, 1)), ImageLockMode.ReadWrite, PixelFormat.Format8bppIndexed);
				
					// Calculate the palette index from our position.
					// If we go beyond the index, fill with the first index. That can only happen when the user chooses a width
					// that isn't a divisor of the bits per pixel which will result in an unfilled final row of colors.
					byte paletteIndex = (byte)((y * swatchWidth) + x);
					if (paletteIndex > paletteIndexSize - 1)
						paletteIndex = 0;

					// Draw a pixel of the current palette index.
					Marshal.WriteByte(bmpData.Scan0, paletteIndex);

					// Unlock the pixel.
					swatch.UnlockBits(bmpData);
				}
			}

			// Save the palette swatch image.
			swatch.Save(outputPalettePath, format);
		}
	}
}

Links