mirror of
https://github.com/spiffcode/hostile-takeover.git
synced 2026-03-29 00:09:40 -06:00
450 lines
12 KiB
C#
450 lines
12 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Drawing;
|
|
using System.Drawing.Imaging;
|
|
using SpiffLib;
|
|
using System.Collections;
|
|
using System.Collections.Specialized;
|
|
|
|
// need limit argument (so fixed pal doesn't go above a certain limit. 128 on 8 bit, 16 on 4
|
|
// need overall palette size argument (can use -p)
|
|
|
|
namespace PalTool
|
|
{
|
|
/// <summary>
|
|
/// Summary description for PalToolApp.
|
|
/// </summary>
|
|
class PalToolApp
|
|
{
|
|
static StringCollection gstrcFileNames = new StringCollection();
|
|
static bool gfPrintHistogram = false;
|
|
static bool gf6bitRGB = false;
|
|
static string gstrOutputFileName = null;
|
|
static bool gfVerbose = false;
|
|
static bool gfEliminateShadowColor = false;
|
|
static bool gfEliminateTransparentColor = false;
|
|
static bool gfPrintColorUsers = false;
|
|
static bool gfAnalyse = false;
|
|
static int giclrInsert = -1;
|
|
static int gcPaletteEntries = 256;
|
|
static int gcColorEntries = 256;
|
|
static bool gfPhotoshopPad = false;
|
|
|
|
struct BitmapColorInfo {
|
|
public string strFileName;
|
|
public Hashtable htColorCount;
|
|
}
|
|
|
|
class ColorCounter : IComparable {
|
|
public int cclr;
|
|
public ArrayList alBitmaps = new ArrayList();
|
|
|
|
// IComparable implementation
|
|
|
|
public int CompareTo(object ob) {
|
|
return cclr - ((ColorCounter)ob).cclr;
|
|
}
|
|
}
|
|
|
|
static void AddFilesFromFile(string strFile) {
|
|
char[] achDelimiter = new char[1];
|
|
achDelimiter[0] = ' ';
|
|
StreamReader sr = new StreamReader(strFile);
|
|
String strLine;
|
|
while ((strLine = sr.ReadLine()) != null) {
|
|
if (strLine.Trim() != "") {
|
|
string[] astrFiles = strLine.Split(achDelimiter);
|
|
for (int i = 0; i < astrFiles.Length; i++) {
|
|
AddFiles(astrFiles[i]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void AddFiles(string strFileArg) {
|
|
string strDir = Path.GetDirectoryName(strFileArg);
|
|
if (strDir == "")
|
|
strDir = ".";
|
|
string[] astrFileNames = Directory.GetFiles(strDir, Path.GetFileName(strFileArg));
|
|
|
|
if (astrFileNames.Length == 0) {
|
|
gstrcFileNames.Add(strFileArg);
|
|
} else {
|
|
foreach (string strFileName in astrFileNames) {
|
|
if (strFileName.ToLower().EndsWith(".ani") || strFileName.ToLower().EndsWith(".amx")) {
|
|
string strT = Path.GetDirectoryName(strFileName) + @"\" + Path.GetFileNameWithoutExtension(strFileName) + @"\*.png";
|
|
string[] astrT = Directory.GetFiles(Path.GetDirectoryName(strT), Path.GetFileName(strT));
|
|
gstrcFileNames.AddRange(astrT);
|
|
} else {
|
|
gstrcFileNames.Add(strFileName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static unsafe int Main(string[] astrArgs) {
|
|
// Command-line argument processing
|
|
|
|
if (astrArgs.Length == 0) {
|
|
PrintHelp();
|
|
return 0;
|
|
}
|
|
|
|
for (int i = 0; i < astrArgs.Length; i++) {
|
|
switch (astrArgs[i]) {
|
|
case "-?":
|
|
PrintHelp();
|
|
return 0;
|
|
|
|
case "-i":
|
|
giclrInsert = Int32.Parse(astrArgs[++i]);
|
|
break;
|
|
|
|
case "-p":
|
|
gcPaletteEntries = Int32.Parse(astrArgs[++i]);
|
|
break;
|
|
|
|
case "-c":
|
|
gcColorEntries = Int32.Parse(astrArgs[++i]);
|
|
break;
|
|
|
|
case "-t":
|
|
gfEliminateTransparentColor = true;
|
|
break;
|
|
|
|
case "-s":
|
|
gfEliminateShadowColor = true;
|
|
break;
|
|
|
|
case "-v":
|
|
gfVerbose = true;
|
|
break;
|
|
|
|
case "-u":
|
|
gfPrintColorUsers = true;
|
|
break;
|
|
|
|
case "-6":
|
|
gf6bitRGB = true;
|
|
break;
|
|
|
|
case "-h":
|
|
gfPrintHistogram = true;
|
|
break;
|
|
|
|
case "-a":
|
|
gfAnalyse = true;
|
|
break;
|
|
|
|
case "-n":
|
|
gfPhotoshopPad = true;
|
|
break;
|
|
|
|
case "-f":
|
|
AddFilesFromFile(astrArgs[++i]);
|
|
break;
|
|
|
|
case "-o":
|
|
if (i + 1 >= astrArgs.Length) {
|
|
Console.WriteLine("Error: -o command requires a filename argument");
|
|
return -1;
|
|
}
|
|
gstrOutputFileName = astrArgs[++i];
|
|
break;
|
|
|
|
default:
|
|
if (astrArgs[i][0] == '-') {
|
|
Console.WriteLine("Error: invalid flag '{0}'", astrArgs[i]);
|
|
return -1;
|
|
}
|
|
|
|
// Assume all 'unassociated' arguments are input filenames (potentially wildcarded)
|
|
|
|
AddFiles(astrArgs[i]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (gstrcFileNames.Count == 0) {
|
|
Console.WriteLine("Error: no files specified");
|
|
return -1;
|
|
}
|
|
|
|
// Build a list of the colors used and count of uses for each bitmap
|
|
|
|
ArrayList alstBitmapColorInfos = new ArrayList();
|
|
|
|
foreach (string strFileName in gstrcFileNames) {
|
|
|
|
BitmapColorInfo bci = new BitmapColorInfo();
|
|
bci.strFileName = strFileName;
|
|
bci.htColorCount = new Hashtable();
|
|
|
|
// Handle .PALs
|
|
|
|
if (strFileName.ToLower().EndsWith(".pal")) {
|
|
Palette pal = new Palette(strFileName);
|
|
|
|
int i = 0;
|
|
foreach (Color clr in pal.Colors) {
|
|
Color clrT = clr;
|
|
if (gf6bitRGB)
|
|
clrT = Color.FromArgb(clr.R & 0xfc, clr.G & 0xfc, clr.B & 0xfc);
|
|
|
|
// This hack causes the .PAL colors to be sorted at the head of the
|
|
// combined palette while retaining the order they were found in the .PAL.
|
|
|
|
if (!bci.htColorCount.Contains(clrT))
|
|
bci.htColorCount[clrT] = (Int32.MaxValue / 2) - i++;
|
|
}
|
|
|
|
// Handle everything else (bitmaps)
|
|
|
|
} else {
|
|
Bitmap bm = null;
|
|
try {
|
|
bm = new Bitmap(strFileName);
|
|
} catch {
|
|
Console.WriteLine("Error: {0} is not a recognized bitmap or palette file", strFileName);
|
|
continue;
|
|
}
|
|
|
|
// Prep to filter out the transparent color
|
|
|
|
Color clrTransparent = Color.GhostWhite;
|
|
if (gfEliminateTransparentColor)
|
|
clrTransparent = bm.GetPixel(0, 0);
|
|
|
|
// Prep to filter out the shadow color
|
|
|
|
Color clrShadow = Color.GhostWhite;
|
|
if (gfEliminateShadowColor)
|
|
clrShadow = Color.FromArgb(156, 212, 248);
|
|
|
|
// Keep a per-bitmap list of unique colors and how many times they're used
|
|
|
|
Hashtable ht = bci.htColorCount;
|
|
|
|
// Lock down bits for speed
|
|
|
|
Rectangle rc = new Rectangle(0, 0, bm.Width, bm.Height);
|
|
BitmapData bmd = bm.LockBits(rc, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
|
|
byte *pbBase = (byte *)bmd.Scan0.ToPointer();
|
|
|
|
for (int y = 0; y < bm.Height; y++) {
|
|
for (int x = 0; x < bm.Width; x++) {
|
|
byte *pb = pbBase + y * bmd.Stride + x * 3;
|
|
Color clr = Color.FromArgb(pb[2], pb[1], pb[0]);
|
|
if (gfEliminateTransparentColor && clr == clrTransparent)
|
|
continue;
|
|
|
|
if (gfEliminateShadowColor && clr == clrShadow)
|
|
continue;
|
|
|
|
if (gf6bitRGB)
|
|
clr = Color.FromArgb(clr.R & 0xfc, clr.G & 0xfc, clr.B & 0xfc);
|
|
|
|
object obT = ht[clr];
|
|
if (obT == null)
|
|
ht[clr] = 1;
|
|
else
|
|
ht[clr] = 1 + (int)obT;
|
|
}
|
|
}
|
|
bm.UnlockBits(bmd);
|
|
}
|
|
|
|
if (gfVerbose)
|
|
Console.WriteLine("{0} uses {1} colors", strFileName, bci.htColorCount.Count);
|
|
|
|
if (gfPrintHistogram && gfVerbose) {
|
|
foreach (DictionaryEntry de in bci.htColorCount) {
|
|
Color clr = (Color)de.Key;
|
|
Console.WriteLine("{0},{1},{2} : {3} occurances", clr.R, clr.G, clr.B, (int)de.Value);
|
|
}
|
|
Console.WriteLine();
|
|
}
|
|
|
|
alstBitmapColorInfos.Add(bci);
|
|
}
|
|
|
|
if (alstBitmapColorInfos.Count == 0) {
|
|
Console.WriteLine("Error: no valid bitmap files to process, terminating");
|
|
return -1;
|
|
}
|
|
|
|
// Combine all the color tables and count data
|
|
|
|
Hashtable htCombined = new Hashtable();
|
|
|
|
foreach (BitmapColorInfo bci in alstBitmapColorInfos) {
|
|
foreach (DictionaryEntry de in bci.htColorCount) {
|
|
Color clr = (Color)de.Key;
|
|
ColorCounter clrc = (ColorCounter)htCombined[clr];
|
|
if (clrc == null) {
|
|
clrc = new ColorCounter();
|
|
clrc.cclr = (int)de.Value;
|
|
htCombined[clr] = clrc;
|
|
} else {
|
|
int nAdd = (int)de.Value;
|
|
if (nAdd > Int32.MaxValue / 3)
|
|
clrc.cclr = (int)de.Value;
|
|
else if (clrc.cclr < Int32.MaxValue / 3)
|
|
clrc.cclr += nAdd;
|
|
}
|
|
clrc.alBitmaps.Add(bci.strFileName);
|
|
}
|
|
}
|
|
|
|
int cclrCombined = htCombined.Count;
|
|
Console.WriteLine("Combined palette has {0} unique colors", cclrCombined);
|
|
|
|
// Sort everything by # colors used
|
|
|
|
ColorCounter[] aclrcSorted = new ColorCounter[cclrCombined];
|
|
// int i = 0;
|
|
// foreach (ColorCounter clrc in htCombined.Values)
|
|
// acOccurancesSorted[i++] = clrc.cclr;
|
|
htCombined.Values.CopyTo(aclrcSorted, 0);
|
|
Color[] aclrSorted = new Color[cclrCombined];
|
|
htCombined.Keys.CopyTo(aclrSorted, 0);
|
|
|
|
Array.Sort(aclrcSorted, aclrSorted);
|
|
|
|
// Reverse so most-used colors come first
|
|
// OPT: could do this inside the Sort above by specifying a custom IComparer
|
|
|
|
Array.Reverse(aclrcSorted);
|
|
Array.Reverse(aclrSorted);
|
|
|
|
if (gfPrintHistogram || gfPrintColorUsers) {
|
|
for (int i = 0; i < cclrCombined; i++) {
|
|
Color clr = aclrSorted[i];
|
|
int cOccurances = aclrcSorted[i].cclr;
|
|
if (cOccurances >= Int32.MaxValue / 3)
|
|
Console.WriteLine("{0},{1},{2} : preloaded", clr.R, clr.G, clr.B);
|
|
else
|
|
Console.WriteLine("{0},{1},{2} : {3} occurances", clr.R, clr.G, clr.B, cOccurances);
|
|
|
|
if (gfPrintColorUsers) {
|
|
foreach (string strFileName in aclrcSorted[i].alBitmaps) {
|
|
Console.WriteLine(" {0}", strFileName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Print warning if # of unique colors is greater than the desired palette size
|
|
// Truncate to match the requested size since other tools depend on this
|
|
|
|
if (cclrCombined > gcColorEntries) {
|
|
Console.WriteLine("Warning! {0} unique colors, {1} palette entries reserved. Truncating...", cclrCombined, gcColorEntries);
|
|
Color[] aclrSortedT = new Color[gcColorEntries];
|
|
Array.Copy(aclrSorted, 0, aclrSortedT, 0, gcColorEntries);
|
|
aclrSorted = aclrSortedT;
|
|
ColorCounter[] aclrcSortedT = new ColorCounter[gcColorEntries];
|
|
Array.Copy(aclrcSorted, 0, aclrcSortedT, 0, gcColorEntries);
|
|
aclrcSorted = aclrcSortedT;
|
|
cclrCombined = gcColorEntries;
|
|
}
|
|
|
|
// Create the palette. Presorted colors start at 0. New colors start at giclrInsert.
|
|
// giclrInsert == -1 means new colors are simply appended to the presorted colors.
|
|
|
|
Color[] aclrPalette = aclrSorted;
|
|
|
|
if (giclrInsert != -1) {
|
|
// Init to transparent
|
|
|
|
aclrPalette = new Color[gcColorEntries];
|
|
for (int i = 0; i < aclrPalette.Length; i++)
|
|
aclrPalette[i] = Color.FromArgb(255, 0, 255);
|
|
|
|
// Insert new colors appropriately
|
|
|
|
int iclrBase = -1;
|
|
for (int i = 0; i < cclrCombined; i++) {
|
|
if (aclrcSorted[i].cclr >= Int32.MaxValue / 3) {
|
|
aclrPalette[i] = aclrSorted[i];
|
|
continue;
|
|
}
|
|
if (iclrBase == -1)
|
|
iclrBase = i;
|
|
int iclrNew = giclrInsert + (i - iclrBase);
|
|
if (iclrNew < aclrPalette.Length)
|
|
aclrPalette[iclrNew] = aclrSorted[i];
|
|
}
|
|
}
|
|
|
|
// Write the output palette file, if requested
|
|
|
|
if (gstrOutputFileName != null) {
|
|
Palette pal = new Palette(aclrPalette);
|
|
if (gfPhotoshopPad)
|
|
pal.Pad(gcPaletteEntries, pal[pal.Length - 1]);
|
|
else
|
|
pal.Pad(gcPaletteEntries, Color.FromArgb(255, 0, 255));
|
|
pal.SaveJasc(gstrOutputFileName);
|
|
}
|
|
|
|
if (gfAnalyse) {
|
|
Palette pal = new Palette(aclrPalette);
|
|
|
|
// For each color find the nearest color in RGB space and print
|
|
// the pair as well as the distance between them.
|
|
|
|
for (int iclrA = 0; iclrA < aclrPalette.Length; iclrA++) {
|
|
Color clrA = aclrPalette[iclrA];
|
|
|
|
// Find the entry, the long way
|
|
|
|
int nLowest = 256 * 256 * 3;
|
|
int iLowest = 0;
|
|
for (int iclr = 0; iclr < aclrPalette.Length; iclr++) {
|
|
if (iclr == iclrA)
|
|
continue;
|
|
|
|
Color clrPal = aclrPalette[iclr];
|
|
int dR = clrPal.R - clrA.R;
|
|
int dG = clrPal.G - clrA.G;
|
|
int dB = clrPal.B - clrA.B;
|
|
int nD = dR * dR + dG * dG + dB * dB;
|
|
if (nD < nLowest) {
|
|
nLowest = nD;
|
|
iLowest = iclr;
|
|
}
|
|
}
|
|
|
|
Color clrB = aclrPalette[iLowest];
|
|
double n = Math.Sqrt(nLowest);
|
|
Console.WriteLine("{8:#.##}\t[{3}] {0},{1},{2} \t[{7}] {4},{5},{6}",
|
|
clrA.R, clrA.G, clrA.B, iclrA, clrB.R, clrB.G, clrB.B, iLowest, n);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
//
|
|
|
|
static void PrintHelp() {
|
|
Console.WriteLine(
|
|
"PalTool usage:\n" +
|
|
"PalTool [-v] [-t] [-s] [-h] [-6] [-o <filename>] file[s]\n" +
|
|
"-v: verbose\n" +
|
|
"-c <count>: color entries that aren't padding\n" +
|
|
"-p <count>: total palette entry count\n" +
|
|
"-i <index>: color index to insert new colors, can be -1\n" +
|
|
"-t: remove transparent color(s) from combined palette\n" +
|
|
"-s: remove shadow color from combined palette\n" +
|
|
"-h: print histogram for combined palette\n" +
|
|
"-u: list which bitmaps use each color\n" +
|
|
"-6: reduce colors to 6-bit RGB before using\n" +
|
|
"-a: analyse the resulting palette and print the results\n" +
|
|
"-n: repeat last entry to pad out to total (good for Photoshop import)\n" +
|
|
"-o <filename>: output combined palette to filename\n" +
|
|
"files[s]: palette, bitmap, ani, and amx files to be processed. Wildcards allowed.");
|
|
}
|
|
}
|
|
}
|