aboutsummaryrefslogtreecommitdiff
path: root/tools/agi-palex.py
blob: 58306dddbeaf7fed4da8cf5df7620df8b920ce0c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#!/usr/bin/python
# Amiga AGI game palette extractor.
# Extracts palette from an Amiga AGI game's executable file.
# Initial version written during summer of 2007 by Buddha^.
# Somewhat optimized. Adding the preliminary palette test helped speed a lot.
# FIXME: Doesn't report anything about not found files when some files are found.
#        An example: palex.py SQ2 PQ1 (When file SQ2 exists but PQ1 doesn't)
import struct, sys, os.path, glob

# Constants
maxComponentValue     = 0xF
colorsPerPalette      = 16
componentsPerColor    = 3
bytesPerComponent     = 2
bytesPerColor         = componentsPerColor   * bytesPerComponent
componentsPerPalette  = colorsPerPalette     * componentsPerColor
bytesPerPalette       = componentsPerPalette * bytesPerComponent
encodedBlack          = '\x00' * bytesPerColor
encodedWhite          = (('\x00' * (bytesPerComponent - 1)) + ("%c" % maxComponentValue)) * componentsPerColor
decodedBlack          = tuple([0 for x in range(componentsPerColor)])
decodedWhite          = tuple([maxComponentValue for x in range(componentsPerColor)])
blackColorNum         = 0
whiteColorNum         = colorsPerPalette - 1
encodedBlackStart     = blackColorNum * bytesPerColor
encodedBlackEnd       = encodedBlackStart + bytesPerColor
encodedWhiteStart     = whiteColorNum * bytesPerColor
encodedWhiteEnd       = encodedWhiteStart + bytesPerColor
componentPrintFormat  = "0x%1X"
arraynamePrefix       = "amigaPalette"

def isColor12Bit(color):
	"""Is the color 12-bit (i.e. 4 bits per color component)?"""
	for component in color:
		if not (0 <= component <= maxComponentValue):
			return False
	return True

def printCommentLineList(lines):
	"""Prints list of lines inside a comment"""
	if len(lines) > 0:
		if len(lines) == 1:
			print "// " + lines[0]
		else:
			print "/**"
			for line in lines:
				print " * " + line
			print " */"

def printColor(color, tabulate = True, printLastComma = True, newLine = True):
	"""Prints color with optional start tabulation, comma in the end and a newline"""
	result = ""
	if tabulate:
		result += "\t"
	for component in color[:-1]:
		result += ((componentPrintFormat + ", ") % component)
	result += (componentPrintFormat % color[-1])
	if printLastComma:
		result += ","
	if newLine:
		print result
	else:
		print result,

def printPalette(palette, filename, arrayname):
	"""Prints out palette as a C-style array"""
	# Print comments about the palette
	comments = ["A 16-color, 12-bit RGB palette from an Amiga AGI game."]
	comments.append("Extracted from file " + os.path.basename(filename))
	printCommentLineList(comments)
	
	# Print the palette as a C-style array
	print "static const unsigned char " + arrayname + "[] = {"
	for color in palette[:-1]:
		printColor(color)
	printColor(palette[-1], printLastComma = False)
	print("};")

def isAmigaPalette(palette):
	"""Test if the given palette is an Amiga-style palette"""
	# Palette must be of correct size
	if len(palette) != colorsPerPalette:
		return False
	
	# First palette color must be black and last palette color must be black
	if palette[whiteColorNum] != decodedWhite or palette[blackColorNum] != decodedBlack:
		return False
	
	# All colors must be 12-bit (i.e. 4 bits per color component)
	for color in palette:
		if not isColor12Bit(color):
			return False
	
	# All colors must be unique
	if len(set(palette)) != colorsPerPalette:
		return False
	
	return True

def preliminaryPaletteTest(data, pos):
	"""Preliminary test for a palette (For speed's sake)."""
	# Test that palette's last color is white
	if data[pos + encodedWhiteStart : pos + encodedWhiteEnd] != encodedWhite:
		return False
	# Test that palette's first color is black
	if data[pos + encodedBlackStart : pos + encodedBlackEnd] != encodedBlack:
		return False
	return True

def searchForAmigaPalettes(filename):
	"""Search file for Amiga AGI game palettes and return any found unique palettes"""
	try:
		file = None
		foundPalettes = []
		# Open file and read it into memory
		file = open(filename, 'rb')
		data = file.read()
		palette = [decodedBlack for x in range(colorsPerPalette)]
		# Search through the whole file
		for searchPosition in range(len(data) - bytesPerPalette + 1):
			if preliminaryPaletteTest(data, searchPosition):
				# Parse possible palette from byte data
				for colorNum in range(colorsPerPalette):
					colorStart = searchPosition + colorNum * bytesPerColor
					colorEnd   = colorStart + bytesPerColor
					# Parse color components as unsigned 16-bit big endian integers
					color = struct.unpack('>' + 'H' * componentsPerColor, data[colorStart:colorEnd])
					palette[colorNum] = color
				# Save good candidates to a list
				if isAmigaPalette(palette):
					foundPalettes.append(tuple(palette))
		# Close source file and return unique found palettes	
		file.close()
		return set(foundPalettes)
	except IOError:
		if file != None:
			file.close()
		return None

# The main program starts here
if len(sys.argv) < 2 or sys.argv[1] == "-h" or sys.argv[1] == "--help":
	quit("Usage: " + os.path.basename(sys.argv[0]) + " FILE [[FILE] ... [FILE]]\n" \
		"  Searches all FILE parameters for Amiga AGI game palettes\n" \
		"  and prints out any found candidates as C-style arrays\n" \
		"  with sequentially numbered names (" + arraynamePrefix + "1, " + arraynamePrefix + "2 etc).\n" \
		"  Supports wildcards.")

# Get the list of filenames (Works with wildcards too)
filenameList = []
for parameter in sys.argv[1:]:
	filenameList.extend(glob.glob(parameter))

# Go through all the files and search for palettes
totalPalettesCount = 0
if len(filenameList) > 0:
	negativeFiles = []
	errorFiles = []
	for filename in filenameList:
		foundPalettes = searchForAmigaPalettes(filename)
		if foundPalettes == None:
			errorFiles.append(filename)
		elif len(foundPalettes) == 0:
			negativeFiles.append(filename)
		else:
			# Print out the found palettes
			for palette in foundPalettes:
				# Print palettes with sequentially numbered array names
				totalPalettesCount = totalPalettesCount + 1
				printPalette(palette, filename, arraynamePrefix + str(totalPalettesCount))
				print "" # Print an extra newline to separate things
	# Print comment about files we couldn't find any palettes in
	if len(negativeFiles) > 0:
		comments = []
		comments.append("Couldn't find any palettes in the following files:")
		comments.extend(negativeFiles)
		printCommentLineList(comments)
		print "" # Print an extra newline to separate things
	# Print comment about errors handling files
	if len(errorFiles) > 0:
		comments = []
		comments.append("Error handling the following files:")
		comments.extend(errorFiles)
		printCommentLineList(comments)
		print "" # Print an extra newline to separate things
else:
	printCommentLineList(["No files found"])