Hooked up the pathfinder so that it seems to work. Animation opcode 0x12.
[scummvm-innocent.git] / tools / themeparser.py
blob7150fa02ed027f3198af5b099486454bc1a4eb35
1 #!/usr/bin/env python
2 # encoding: utf-8
4 """
5 " ScummVM - Graphic Adventure Engine
7 " ScummVM is the legal property of its developers, whose names
8 " are too numerous to list here. Please refer to the COPYRIGHT
9 " file distributed with this source distribution.
11 " This program is free software; you can redistribute it and/or
12 " modify it under the terms of the GNU General Public License
13 " as published by the Free Software Foundation; either version 2
14 " of the License, or (at your option) any later version.
16 " This program is distributed in the hope that it will be useful,
17 " but WITHOUT ANY WARRANTY; without even the implied warranty of
18 " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 " GNU General Public License for more details.
21 " You should have received a copy of the GNU General Public License
22 " along with this program; if not, write to the Free Software
23 " Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
25 " $URL$
26 " $Id$
27 """
29 from __future__ import with_statement
30 import os
31 import xml.dom.minidom as DOM
32 import struct
34 FILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in range(256)])
36 # adapted from Activestate Snippet Cookbook
37 def printBinaryDump(src, length=16):
38 N=0; result=''
39 while src:
40 s,src = src[:length],src[length:]
41 hexa = ' '.join(["%02X"%ord(x) for x in s])
42 s = s.translate(FILTER)
43 result += "%04X %-*s %s\n" % (N, length*3, hexa, s)
44 N+=length
45 print (result)
47 def pbin(data):
48 return str(map(lambda c: hex(ord(c)), data))
49 # return " ".join(["%0.2X" % ord(c) for c in data])
51 class STXBinaryFile(object):
52 class InvalidRGBColor(Exception):
53 pass
55 class InvalidResolution(Exception):
56 pass
58 class InvalidFontIdentifier(Exception):
59 pass
61 class InvalidBitmapName(Exception):
62 pass
64 class InvalidDialogOverlay(Exception):
65 pass
67 class DrawStepData(object):
68 def __init__(self, isDefault, packFormat, function):
69 self.isDefault = isDefault
70 self.packFormat = packFormat
71 self.parse = function
73 BYTEORDER = '>' # big-endian format by default, platform independent
75 TRUTH_VALUES = {"" : 0, "true" : 1, "false" : 0, "True" : 1, "False" : 0}
77 BLOCK_HEADERS = {
78 "bitmaps" : 0x0100000A,
79 "fonts" : 0x0100000B,
80 "cursor" : 0x0100000C,
81 "drawdata" : 0x0100000D,
83 "globals" : 0x0200000A,
84 "dialog" : 0x0200000B,
87 DIALOG_shading = {"none" : 0x0, "dim" : 0x1, "luminance" : 0x2}
89 DS_triangleOrientations = {"top" : 0x1, "bottom" : 0x2, "left" : 0x3, "right" : 0x4}
90 DS_fillModes = {"none" : 0x0, "foreground" : 0x1, "background" : 0x2, "gradient" : 0x3}
91 DS_vectorAlign = {"left" : 0x1, "right" : 0x2, "bottom" : 0x3, "top" : 0x4, "center" : 0x5}
93 DS_functions = {
94 "void" : 0x0, "circle" : 0x1, "square" : 0x2, "roundedsq" : 0x3, "bevelsq" : 0x4,
95 "line" : 0x5, "triangle" : 0x6, "fill" : 0x7, "tab" : 0x8, "bitmap" : 0x9, "cross" : 0xA
98 DS_fonts = {
99 "text_default" : 0x0, "text_hover" : 0x1, "text_disabled" : 0x2,
100 "text_inverted" : 0x3, "text_button" : 0x4, "text_button_hover" : 0x5, "text_normal" : 0x6
103 DS_text_alignV = {"bottom" : 0x0, "center" : 0x1, "top" : 0x2}
104 DS_text_alignH = {"left" : 0x0, "center" : 0x1, "right" : 0x2}
106 DS_list = [
107 "func", "fill", "stroke", "gradient_factor",
108 "width", "height", "xpos", "ypos",
109 "radius", "bevel", "shadow", "orientation", "file",
110 "fg_color", "bg_color", "gradient_start", "gradient_end", "bevel_color"
113 def __init__(self, themeName, autoLoad = True, verbose = False):
114 self._themeName = themeName
115 self._stxFiles = []
116 self._verbose = verbose
118 self.DS_data = {
119 # attribute name isDefault pack parse function
120 "func" : self.DrawStepData(False, "B", lambda f: self.DS_functions[f]),
121 "fill" : self.DrawStepData(True, "B", lambda f: self.DS_fillModes[f]),
122 "stroke" : self.DrawStepData(True, "B", int),
123 "gradient_factor" : self.DrawStepData(True, "B", int),
124 "width" : self.DrawStepData(False, "i", lambda w: -1 if w == 'height' else 0 if w == 'auto' else int(w)),
125 "height" : self.DrawStepData(False, "i", lambda h: -1 if h == 'width' else 0 if h == 'auto' else int(h)),
126 "xpos" : self.DrawStepData(False, "i", lambda pos: self.DS_vectorAlign[pos] if pos in self.DS_vectorAlign else int(pos)),
127 "ypos" : self.DrawStepData(False, "i", lambda pos: self.DS_vectorAlign[pos] if pos in self.DS_vectorAlign else int(pos)),
128 "radius" : self.DrawStepData(False, "i", lambda r: 0xFF if r == 'auto' else int(r)),
129 "bevel" : self.DrawStepData(True, "B", int),
130 "shadow" : self.DrawStepData(True, "B", int),
131 "orientation" : self.DrawStepData(False, "B", lambda o: self.DS_triangleOrientations[o]),
132 "file" : self.DrawStepData(False, "B", self.__getBitmap),
133 "fg_color" : self.DrawStepData(True, "4s", self.__parseColor),
134 "bg_color" : self.DrawStepData(True, "4s", self.__parseColor),
135 "gradient_start" : self.DrawStepData(True, "4s", self.__parseColor),
136 "gradient_end" : self.DrawStepData(True, "4s", self.__parseColor),
137 "bevel_color" : self.DrawStepData(True, "4s", self.__parseColor)
140 if autoLoad:
141 if not os.path.isdir(themeName) or not os.path.isfile(os.path.join(themeName, "THEMERC")):
142 raise IOError
144 for filename in os.listdir(themeName):
145 filename = os.path.join(themeName, filename)
146 if os.path.isfile(filename) and filename.endswith('.stx'):
147 self._stxFiles.append(filename)
149 def debug(self, text):
150 if self._verbose: print (text)
152 def debugBinary(self, data):
153 if self._verbose:
154 print ("BINARY OUTPUT (%d bytes): %s" % (len(data), " ".join(["%0.2X" % ord(c) for c in data])))
156 def addSTXFile(self, filename):
157 if not os.path.isfile(filename):
158 raise IOError
159 else:
160 self._stxFiles.append(filename)
162 def parse(self):
163 if not self._stxFiles:
164 self.debug("No files have been loaded for parsing on the theme.")
165 raise IOError
167 for f in self._stxFiles:
168 self.debug("Parsing %s." % f)
169 with open(f) as stxFile:
170 self.__parseFile(stxFile)
172 def __parseFile(self, xmlFile):
173 stxDom = DOM.parse(xmlFile)
175 for layout in stxDom.getElementsByTagName("layout_info"):
176 self.__parseLayout(layout)
178 for render in stxDom.getElementsByTagName("render_info"):
179 self.__parseRender(render)
181 stxDom.unlink()
183 def __getBitmap(self, bmp):
184 bmp = str(bmp)
186 if bmp == "":
187 return 0x0
188 if bmp not in self._bitmaps:
189 raise self.InvalidBitmapName
191 return self._bitmaps[bmp]
193 def __parseDrawStep(self, drawstepDom, localDefaults = {}):
195 dstable = {}
197 if drawstepDom.tagName == "defaults":
198 isGlobal = drawstepDom.parentNode.tagName == "render_info"
200 for ds in self.DS_list:
201 if self.DS_data[ds].isDefault and drawstepDom.hasAttribute(ds):
202 dstable[ds] = self.DS_data[ds].parse(drawstepDom.getAttribute(ds))
204 elif isGlobal:
205 dstable[ds] = None
207 else:
208 for ds in self.DS_data:
209 if drawstepDom.hasAttribute(ds):
210 dstable[ds] = self.DS_data[ds].parse(drawstepDom.getAttribute(ds))
211 elif self.DS_data[ds].isDefault:
212 dstable[ds] = localDefaults[ds] if ds in localDefaults else self._globalDefaults[ds]
213 else:
214 dstable[ds] = None
216 return dstable
219 def __parseDrawStepToBin(self, stepDict):
221 /BBBBiiiiiBBB4s4s4s4s4sB/ ==
222 function (byte)
223 fill (byte)
224 stroke (byte)
225 gradient_factor (byte)
226 width (int32)
227 height (int32)
228 xpos (int32)
229 ypos (int32)
230 radius (int32)
231 bevel (byte)
232 shadow (byte)
233 orientation (byte)
234 file (byte)
235 fg_color (4 byte)
236 bg_color (4 byte)
237 gradient_start (4 byte)
238 gradient_end (4 byte)
239 bevel_color (4 byte)
242 packLayout = ""
243 packData = []
245 for ds in self.DS_list:
246 layout = self.DS_data[ds].packFormat
247 data = stepDict[ds]
249 if not data:
250 size = struct.calcsize(layout)
251 packLayout += "B" * size
253 for d in range(size):
254 packData.append(0)
255 else:
256 packLayout += layout
257 packData.append(data)
260 stepBin = struct.pack(self.BYTEORDER + packLayout, *tuple(packData))
261 return stepBin
264 def __parseResolutionToBin(self, resString):
266 /B bHH bHH bHH/ == 1 byte + x * 9 bytes
267 number of resolution sections (byte)
268 exclude resolution (byte)
269 resolution X (half)
270 resolution Y (half)
273 if resString == "":
274 return struct.pack(self.BYTEORDER + "BbHH", 1, 0, 0, 0)
276 resolutions = resString.split(", ")
277 packFormat = "B" + "bHH" * len(resolutions)
278 packData = [len(resolutions)]
280 for res in resolutions:
281 exclude = 0
282 if res[0] == '-':
283 exclude = 1
284 res = res[1:]
286 try:
287 x, y = res.split('x')
288 x = 0 if x == 'X' else int(x)
289 y = 0 if y == 'Y' else int(y)
290 except ValueError:
291 raise InvalidResolution
293 packData.append(exclude)
294 packData.append(x)
295 packData.append(y)
297 buff = struct.pack(self.BYTEORDER + packFormat, *tuple(packData))
298 return buff
300 def __parseRGBToBin(self, color):
302 /xBBB/ == 32 bits
303 padding (byte)
304 red color (byte)
305 green color (byte)
306 blue color (byte)
309 try:
310 rgb = tuple(map(int, color.split(", ")))
311 except ValueError:
312 raise self.InvalidRGBColor
314 if len(rgb) != 3:
315 raise self.InvalidRGBColor
317 for c in rgb:
318 if c < 0 or c > 255:
319 raise self.InvalidRGBColor
321 rgb = struct.pack(self.BYTEORDER + "xBBB", *tuple(rgb))
323 # self.debugBinary(rgb)
324 return rgb
326 def __parseColor(self, color):
327 try:
328 color = self.__parseRGBToBin(color)
329 except self.InvalidRGBColor:
330 if color not in self._colors:
331 raise self.InvalidRGBColor
332 color = self._colors[color]
334 return color
337 def __parsePalette(self, paletteDom):
338 self._colors = {}
340 for color in paletteDom.getElementsByTagName("color"):
341 color_name = color.getAttribute('name')
342 color_rgb = self.__parseRGBToBin(color.getAttribute('rgb'))
344 self._colors[color_name] = color_rgb
345 # self.debug("COLOR: %s" % (color_name))
348 def __parseBitmaps(self, bitmapsDom):
349 self._bitmaps = {}
350 idCount = 0xA0
351 packLayout = ""
352 packData = []
354 for bitmap in bitmapsDom.getElementsByTagName("bitmap"):
355 bmpName = str(bitmap.getAttribute("filename"))
356 self._bitmaps[bmpName] = idCount
358 packLayout += "B%ds" % (len(bmpName) + 1)
359 packData.append(idCount)
360 packData.append(bmpName)
361 idCount += 1
364 bitmapBinary = struct.pack(
365 self.BYTEORDER + "IB" + packLayout,
366 self.BLOCK_HEADERS['bitmaps'],
367 len(bitmapsDom.getElementsByTagName("bitmap")),
368 *tuple(packData)
371 # self.debug("BITMAPS:\n%s\n\n" % pbin(bitmapBinary))
372 return bitmapBinary
374 def __parseFonts(self, fontsDom):
376 /IB Bs4ss .../
377 section header (uint32)
378 number of font definitions (byte)
380 id for font definition (byte)
381 resolution for font (byte array)
382 color for font (4 bytes)
383 font filename (byte array, null terminated)
386 packLayout = ""
387 packData = []
389 for font in fontsDom.getElementsByTagName("font"):
390 ident = font.getAttribute("id")
392 if ident not in self.DS_fonts:
393 raise self.InvalidFontIdentifier
395 color = self.__parseColor(font.getAttribute("color"))
396 filename = str(font.getAttribute("file"))
398 if filename == 'default':
399 filename = ''
401 resolution = self.__parseResolutionToBin(font.getAttribute("resolution"))
403 packLayout += "B%ds4s%ds" % (len(resolution), len(filename) + 1)
404 packData.append(self.DS_fonts[ident])
405 packData.append(resolution)
406 packData.append(color)
407 packData.append(filename)
409 fontsBinary = struct.pack(self.BYTEORDER + \
410 "IB" + packLayout,
411 self.BLOCK_HEADERS['fonts'],
412 len(fontsDom.getElementsByTagName("font")),
413 *tuple(packData)
417 # self.debug("FONTS DATA:\n%s\n\n" % pbin(fontsBinary))
418 return fontsBinary
420 def __parseTextToBin(self, textDom):
422 /BBBx/
423 font identifier (byte)
424 vertical alignment (byte)
425 horizontal alignment (byte)
426 padding until word (byte)
429 font = textDom.getAttribute("font")
430 if font not in self.DS_fonts:
431 raise self.InvalidFontIdentifier
433 textBin = struct.pack(self.BYTEORDER + "BBBx",
434 self.DS_fonts[font],
435 self.DS_text_alignV[textDom.getAttribute("vertical_align")],
436 self.DS_text_alignH[textDom.getAttribute("horizontal_align")]
439 return textBin
441 def __parseDrawData(self, ddDom):
443 /IsIBBHss/
444 Section Header (uint32)
445 Resolution (byte array, word-aligned)
446 DrawData id hash (uint32)
447 Cached (byte)
448 has text section? (byte)
449 number of DD sections (uint16)
450 ** text segment (4 bytes)
451 drawstep segments (byte array)
455 localDefaults = ddDom.getElementsByTagName("defaults")
456 localDefaults = localDefaults[0] if localDefaults else {}
458 stepList = []
460 for ds in ddDom.getElementsByTagName("drawstep"):
461 dstable = self.__parseDrawStep(ds, localDefaults)
462 dsbinary = self.__parseDrawStepToBin(dstable)
464 stepList.append(dsbinary)
466 stepByteArray = "".join(stepList)
468 resolution = self.__parseResolutionToBin(ddDom.getAttribute("resolution"))
470 text = ddDom.getElementsByTagName("text")
471 text = self.__parseTextToBin(text[0]) if text else ""
473 id_hash = str.__hash__(str(ddDom.getAttribute("id"))) & 0xFFFFFFFF
474 cached = self.TRUTH_VALUES[ddDom.getAttribute("cached")]
476 ddBinary = struct.pack(self.BYTEORDER + \
477 "I%dsIBBH4s%ds" % (len(resolution), len(stepByteArray)),
479 self.BLOCK_HEADERS['drawdata'], # Section Header (uint32)
480 resolution, # Resolution (byte array, word-aligned)
481 id_hash, # DrawData id hash (uint32)
482 cached, # Cached (byte)
483 0x1 if text else 0x0, # has text section? (byte)
484 len(stepList), # number of DD sections (uint16)
485 text, # ** text segment (byte array)
486 stepByteArray # drawstep segments (byte array)
489 # self.debug("DRAW DATA %s (%X): \n" % (ddDom.getAttribute("id"), id_hash) + pbin(ddBinary) + "\n\n")
490 return ddBinary
492 def __parseCursor(self, cursorDom):
494 /IsBBhh/
495 section header (uint32)
496 resolution string (byte array)
497 bitmap id (byte)
498 scale (byte)
499 hotspot X (half)
500 hotspot Y (half)
503 resolution = self.__parseResolutionToBin(cursorDom.getAttribute("resolution"))
504 scale = int(cursorDom.getAttribute("scale"))
505 hsX, hsY = cursorDom.getAttribute("hotspot").split(", ")
507 cursorBin = struct.pack(self.BYTEORDER + "I%dsBBhh" % len(resolution),
508 self.BLOCK_HEADERS['cursor'],
509 resolution,
510 self.__getBitmap(cursorDom.getAttribute("file")),
511 scale,
512 int(hsX),
513 int(hsY)
516 # self.debug("CURSOR:\n%s\n\n" % pbin(cursorBin))
517 return cursorBin
519 def __parseDialog(self, dialogDom):
521 dialog_id = str(dialogDom.getAttribute("name"))
522 resolution = self.__parseResolutionToBin(dialogDom.getAttribute("resolution"))
524 overlays = str(dialogDom.getAttribute("overlays"))
525 overlay_type = 0x0
526 overlay_parent = ""
528 if overlays == "screen":
529 overlay_type = 0x1
530 elif overlays == "screen_center":
531 overlay_type = 0x2
532 else:
533 overlay_type = 0x3
534 overlay_parent = str(overlays)
536 dialog_enabled = 0x1
537 if dialogDom.hasAttribute("enabled"):
538 dialog_enabled = self.TRUTH_VALUES[dialogDom.getAttribute("enabled")]
540 dialog_shading = 0x0
541 if dialogDom.hasAttribute("shading"):
542 dialog_shading = self.DIALOG_shading[dialogDom.getAttribute("shading")]
544 dialog_inset = 0
545 if dialogDom.hasAttribute("inset"):
546 dialog_inset = int(dialogDom.getAttribute("inset"))
548 dialogBin = struct.pack(self.BYTEORDER + \
549 "I%ds%dsBBBB%ds" % (len(resolution), len(dialog_id) + 1, len(overlay_parent) + 1),
550 self.BLOCK_HEADERS['dialog'],
551 resolution,
552 dialog_id,
553 dialog_enabled,
554 dialog_shading,
555 dialog_inset,
556 overlay_type,
557 overlay_parent,
560 return dialogBin
562 def __parseLayout(self, layoutDom):
563 self.debug("GLOBAL SECTION: LAYOUT INFO.")
565 dialogBIN = ""
567 for dialog in layoutDom.getElementsByTagName("dialog"):
568 dialogBIN += self.__parseDialog(dialog)
571 printBinaryDump(dialogBIN)
573 return dialogBIN
575 def __parseRender(self, renderDom):
576 self.debug("GLOBAL SECTION: RENDER INFO.")
578 bitmapBIN = ""
579 fontsBIN = ""
580 cursorBIN = ""
581 drawdataBIN = ""
583 # parse color palettes
584 paletteDom = renderDom.getElementsByTagName("palette")
585 if paletteDom:
586 self.__parsePalette(paletteDom[0])
588 # parse bitmaps
589 bitmapsDom = renderDom.getElementsByTagName("bitmaps")
590 if bitmapsDom:
591 bitmapBIN = self.__parseBitmaps(bitmapsDom[0])
593 # parse fonts
594 fontsDom = renderDom.getElementsByTagName("fonts")[0]
595 fontsBIN = self.__parseFonts(fontsDom)
597 # parse defaults
598 defaultsDom = renderDom.getElementsByTagName("defaults")
599 if defaultsDom:
600 self._globalDefaults = self.__parseDrawStep(defaultsDom[0])
601 else:
602 self._globalDefaults = {}
604 # parse cursors
605 for cur in renderDom.getElementsByTagName("cursor"):
606 cursorBIN += self.__parseCursor(cur)
608 # parse drawdata sets
609 for dd in renderDom.getElementsByTagName("drawdata"):
610 drawdataBIN += self.__parseDrawData(dd)
613 renderInfoBIN = bitmapBIN + fontsBIN + cursorBIN + drawdataBIN
614 printBinaryDump(renderInfoBIN)
616 return renderInfoBIN
618 if __name__ == '__main__':
619 bin = STXBinaryFile('../gui/themes/scummclassic', True, True)
620 bin.parse()