scummvm/tools/themeparser.py

429 lines
12 KiB
Python
Raw Normal View History

#!/usr/bin/env python
# encoding: utf-8
"""
" ScummVM - Graphic Adventure Engine
"
" ScummVM is the legal property of its developers, whose names
" are too numerous to list here. Please refer to the COPYRIGHT
" file distributed with this source distribution.
"
" This program is free software; you can redistribute it and/or
" modify it under the terms of the GNU General Public License
" as published by the Free Software Foundation; either version 2
" of the License, or (at your option) any later version.
"
" This program is distributed in the hope that it will be useful,
" but WITHOUT ANY WARRANTY; without even the implied warranty of
" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
" GNU General Public License for more details.
"
" You should have received a copy of the GNU General Public License
" along with this program; if not, write to the Free Software
" Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"
" $URL$
" $Id$
"""
from __future__ import with_statement
import os
import xml.dom.minidom as DOM
import struct
def pbin(data):
return " ".join(["%0.2X" % ord(c) for c in data])
class STXBinaryFile:
class InvalidRGBColor(Exception):
pass
class InvalidResolution(Exception):
pass
TRUTH_VALUES = {"" : 0, "true" : 1, "false" : 0, "True" : 1, "False" : 0}
BLOCK_HEADERS = {
"palette" : 0x0100000A,
"bitmaps" : 0x0100000B,
"fonts" : 0x0100000C,
"defaults" : 0x0100000D,
"cursor" : 0x0100000E,
"drawdata" : 0x0100000F
}
DRAWSTEP_FORMAT_DEF = [
"stroke", "shadow",
"bevel", "gradient_factor",
"fg_color", "bg_color",
"fill", "gradient_start",
"gradient_end", "bevel_color",
]
DRAWSTEP_FORMAT = [
"func", "radius", "width", "height",
"xpos", "ypos", "file", "orientation",
] + DRAWSTEP_FORMAT_DEF
def __init__(self, themeName, autoLoad = True, verbose = False):
self._themeName = themeName
self._stxFiles = []
self._verbose = verbose
if autoLoad:
if not os.path.isdir(themeName) or not os.path.isfile(os.path.join(themeName, "THEMERC")):
raise IOError
for filename in os.listdir(themeName):
filename = os.path.join(themeName, filename)
if os.path.isfile(filename) and filename.endswith('.stx'):
self._stxFiles.append(filename)
def debug(self, text):
if self._verbose: print text
def debugBinary(self, data):
if self._verbose:
print "BINARY OUTPUT (%d bytes):" % len(data), " ".join(["%0.2X" % ord(c) for c in data])
def addSTXFile(self, filename):
if not os.path.isfile(filename):
raise IOError
else:
self._stxFiles.append(filename)
def parse(self):
if not self._stxFiles:
self.debug("No files have been loaded for parsing on the theme.")
raise IOError
for f in self._stxFiles:
self.debug("Parsing %s." % f)
with open(f) as stxFile:
self.__parseFile(stxFile)
def __parseFile(self, xmlFile):
stxDom = DOM.parse(xmlFile)
for layout in stxDom.getElementsByTagName("layout_info"):
self.__parseLayout(layout)
for render in stxDom.getElementsByTagName("render_info"):
self.__parseRender(render)
stxDom.unlink()
def __parseDrawStep(self, drawstepDom, localDefaults = {}):
triangleOrientations = {"top" : 0x1, "bottom" : 0x2, "left" : 0x3, "right" : 0x4}
fillModes = {"none" : 0x0, "foreground" : 0x1, "background" : 0x2, "gradient" : 0x3}
vectorAlign = {"left" : 0x1, "right" : 0x2, "bottom" : 0x3, "top" : 0x4, "center" : 0x5}
functions = {
"void" : 0x0, "circle" : 0x1, "square" : 0x2, "roundedsq" : 0x3, "bevelsq" : 0x4,
"line" : 0x5, "triangle" : 0x6, "fill" : 0x7, "tab" : 0x8, "bitmap" : 0x9, "cross" : 0xA
}
parseAttribute = {
"stroke" : int,
"bevel" : int,
"shadow" : int,
"gradient_factor" : int,
"fg_color" : self.__parseColor,
"bg_color" : self.__parseColor,
"gradient_start" : self.__parseColor,
"gradient_end" : self.__parseColor,
"bevel_color" : self.__parseColor,
"radius" : lambda r: 0xFF if r == 'auto' else int(r),
"file" : str,
"orientation" : lambda o: triangleOrientations[o],
"fill" : lambda f: fillModes[f],
"func" : lambda f: functions[f],
"width" : lambda w: -1 if w == 'height' else 0 if w == 'auto' else int(w),
"height" : lambda h: -1 if h == 'width' else 0 if h == 'auto' else int(h),
"xpos" : lambda pos: vectorAlign[pos] if pos in vectorAlign else int(pos),
"ypos" : lambda pos: vectorAlign[pos] if pos in vectorAlign else int(pos),
}
dstable = {}
if drawstepDom.tagName == "defaults":
isGlobal = drawstepDom.parentNode.tagName == "render_info"
for attr in self.DRAWSTEP_FORMAT_DEF:
if drawstepDom.hasAttribute(attr):
dstable[attr] = parseAttribute[attr](drawstepDom.getAttribute(attr))
elif isGlobal:
dstable[attr] = None
else:
for attr in self.DRAWSTEP_FORMAT:
if drawstepDom.hasAttribute(attr):
dstable[attr] = parseAttribute[attr](drawstepDom.getAttribute(attr))
elif attr in self.DRAWSTEP_FORMAT_DEF:
dstable[attr] = localDefaults[attr] if attr in localDefaults else self._globalDefaults[attr]
else:
dstable[attr] = None
return dstable
def __parseDrawStepToBin(self, stepDict):
"""
/BBBBiiiiBBBB4s4s4s4s4ss/ == 32 + 4 * 32 + 32 + 5 * 32 = 352 / 4 bits = 88 bytes + var string
function (byte)
fill (byte)
stroke (byte)
gradient_factor (byte)
width (int32)
height (int32)
xpos (int32)
ypos (int32)
radius (byte)
bevel (byte)
shadow (byte)
orientation (byte)
fg_color (4 byte)
bg_color (4 byte)
gradient_start (4 byte)
gradient_end (4 byte)
bevel_color (4 byte)
file (byte string, null terminated)
"""
DRAWSTEP_BININFO = {
"stroke" : "B", "shadow" : "B", "bevel" : "B",
"fill" : "B", "func" : "B", "radius" : "b",
"width" : "i", "height" : "i", "xpos" : "i",
"ypos" : "i", "orientation" : "B", "file" : "%ds",
"fg_color" : "4s", "bg_color" : "4s",
"gradient_start" : "4s", "gradient_end" : "4s",
"bevel_color" : "4s", "gradient_factor" : "B"
}
packLayout = ""
packData = []
for attr in self.DRAWSTEP_FORMAT:
layout = DRAWSTEP_BININFO[attr]
data = stepDict[attr]
if layout == "%ds":
if data:
packLayout += layout % (len(data) + 1)
packData.append(data)
else:
packLayout += "B"
packData.append(0)
elif not data:
size = struct.calcsize(layout)
packLayout += "B" * size
for d in xrange(size):
packData.append(0)
else:
packLayout += layout
packData.append(data)
stepBin = struct.pack(packLayout, *tuple(packData))
# self.debugBinary(stepBin)
return stepBin
def __parseResolutionToBin(self, resString):
"""
/i xxxbII xxxbII xxxbII/ == 32 bits + x * 32 bits
number of resolution sections (int32)
padding (byte)
exclude resolution (byte)
resolution X (int32)
resolution Y (int32)
"""
if resString == "":
return struct.pack("ixxxbII", 1, 0, 0, 0)
resolutions = resString.split(", ")
packFormat = "i" + "xxxbII" * len(resolutions)
packData = [len(resolutions)]
for res in resolutions:
exclude = 0
if res[0] == '-':
exclude = 1
res = res[1:]
try:
x, y = res.split('x')
x = 0 if x == 'X' else int(x)
y = 0 if y == 'Y' else int(y)
except ValueError:
raise InvalidResolution
packData.append(exclude)
packData.append(x)
packData.append(y)
buff = struct.pack(packFormat, *tuple(packData))
self.debug("Pack format: %s => %s" % (packFormat, str(packData)))
# self.debugBinary(buff)
return buff
def __parseRGBToBin(self, color):
"""
/xBBB/ == 32 bits
padding (byte)
red color (byte)
green color (byte)
blue color (byte)
"""
try:
rgb = tuple(map(int, color.split(", ")))
except ValueError:
raise self.InvalidRGBColor
if len(rgb) != 3:
raise self.InvalidRGBColor
for c in rgb:
if c < 0 or c > 255:
raise self.InvalidRGBColor
rgb = struct.pack("xBBB", *tuple(rgb))
# self.debugBinary(rgb)
return rgb
def __parseColor(self, color):
try:
color = self.__parseRGBToBin(color)
except self.InvalidRGBColor:
if color not in self._colors:
raise self.InvalidRGBColor
color = self._colors[color]
return color
def __parsePalette(self, paletteDom):
self._colors = {}
for color in paletteDom.getElementsByTagName("color"):
color_name = color.getAttribute('name')
color_rgb = self.__parseRGBToBin(color.getAttribute('rgb'))
self._colors[color_name] = color_rgb
self.debug("COLOR: %s" % (color_name))
def __parseBitmaps(self, bitmapsDom):
self._bitmaps = []
for bitmap in bitmapsDom.getElementsByTagName("bitmap"):
bmpName = bitmap.getAttribute("filename")
resolution = self.__parseResolutionToBin(bitmap.getAttribute("resolution"))
self._bitmaps.append((bmpName, resolution))
self.debug("BITMAP: %s" % bmpName)
def __parseFonts(self, fontsDom):
self._fonts = []
for font in fontsDom.getElementsByTagName("font"):
ident = font.getAttribute("id")
color = self.__parseColor(font.getAttribute("color"))
filename = font.getAttribute("file")
resolution = self.__parseResolutionToBin(font.getAttribute("resolution"))
self.debug("FONT: %s @ %s" % (ident, filename))
self._fonts.append((ident, filename, color, resolution))
def __parseTextToBin(self, textDom):
return ""
def __parseDrawData(self, ddDom):
"""
/IsIBBHss/
Section Header (uint32)
Resolution (byte array, word-aligned)
DrawData id hash (uint32)
Cached (byte)
has text section? (byte)
number of DD sections (uint16)
** text segment (byte array)
drawstep segments (byte array)
"""
localDefaults = ddDom.getElementsByTagName("defaults")
localDefaults = localDefaults[0] if localDefaults else {}
stepList = []
for ds in ddDom.getElementsByTagName("drawstep"):
dstable = self.__parseDrawStep(ds, localDefaults)
dsbinary = self.__parseDrawStepToBin(dstable)
stepList.append(dsbinary)
stepByteArray = "".join(stepList)
resolution = self.__parseResolutionToBin(ddDom.getAttribute("resolution"))
text = ddDom.getElementsByTagName("text")
text = self.__parseTextToBin(text[0]) if text else ""
id_hash = str.__hash__(str(ddDom.getAttribute("id")))
cached = self.TRUTH_VALUES[ddDom.getAttribute("cached")]
ddBinary = struct.pack(
"I%dsiBBH%ds%ds" % (len(resolution), len(text), len(stepByteArray)),
self.BLOCK_HEADERS['drawdata'], # Section Header (uint32)
resolution, # Resolution (byte array, word-aligned)
id_hash, # DrawData id hash (uint32)
cached, # Cached (byte)
0x1 if text else 0x0, # has text section? (byte)
len(stepList), # number of DD sections (uint16)
text, # ** text segment (byte array)
stepByteArray # drawstep segments (byte array)
)
self.debug("DRAW DATA %s (%X): \n" % (ddDom.getAttribute("id"), id_hash) + pbin(ddBinary) + "\n\n")
return ddBinary
def __parseLayout(self, layoutDom):
self.debug("GLOBAL SECTION: LAYOUT INFO.")
def __parseRender(self, renderDom):
self.debug("GLOBAL SECTION: RENDER INFO.")
paletteDom = renderDom.getElementsByTagName("palette")[0]
bitmapsDom = renderDom.getElementsByTagName("bitmaps")[0]
fontsDom = renderDom.getElementsByTagName("fonts")[0]
defaultsDom = renderDom.getElementsByTagName("defaults")[0]
self.__parsePalette(paletteDom)
self.__parseBitmaps(bitmapsDom)
self.__parseFonts(fontsDom)
self._globalDefaults = self.__parseDrawStep(defaultsDom)
for dd in renderDom.getElementsByTagName("drawdata"):
self.__parseDrawData(dd)
if __name__ == '__main__':
bin = STXBinaryFile('../gui/themes/scummmodern', True, True)
bin.parse()