mirror of
https://github.com/holub/mame
synced 2025-04-25 17:56:43 +03:00
381 lines
14 KiB
Python
381 lines
14 KiB
Python
#!/usr/bin/python
|
|
##
|
|
## license:BSD-3-Clause
|
|
## copyright-holders:Vas Crabb
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
import xml.sax
|
|
import xml.sax.saxutils
|
|
import zlib
|
|
|
|
|
|
# workaround for version incompatibility
|
|
if sys.version_info > (3, ):
|
|
long = int
|
|
|
|
|
|
class ErrorHandler(object):
|
|
def __init__(self, **kwargs):
|
|
super(ErrorHandler, self).__init__(**kwargs)
|
|
self.errors = 0
|
|
self.warnings = 0
|
|
|
|
def error(self, exception):
|
|
self.errors += 1
|
|
sys.stderr.write('error: %s' % (exception))
|
|
|
|
def fatalError(self, exception):
|
|
raise exception
|
|
|
|
def warning(self, exception):
|
|
self.warnings += 1
|
|
sys.stderr.write('warning: %s' % (exception))
|
|
|
|
|
|
class Minifyer(object):
|
|
def __init__(self, output, **kwargs):
|
|
super(Minifyer, self).__init__(**kwargs)
|
|
|
|
self.output = output
|
|
self.incomplete_tag = False
|
|
self.element_content = ''
|
|
|
|
def setDocumentLocator(self, locator):
|
|
pass
|
|
|
|
def startDocument(self):
|
|
self.output('<?xml version="1.0"?>')
|
|
|
|
def endDocument(self):
|
|
self.output('\n')
|
|
|
|
def startElement(self, name, attrs):
|
|
self.flushElementContent()
|
|
if self.incomplete_tag:
|
|
self.output('>')
|
|
self.output('<%s' % (name))
|
|
for name in attrs.getNames():
|
|
self.output(' %s=%s' % (name, xml.sax.saxutils.quoteattr(attrs[name])))
|
|
self.incomplete_tag = True
|
|
|
|
def endElement(self, name):
|
|
self.flushElementContent()
|
|
if self.incomplete_tag:
|
|
self.output('/>')
|
|
else:
|
|
self.output('</%s>' % (name))
|
|
self.incomplete_tag = False
|
|
|
|
def characters(self, content):
|
|
self.element_content += content
|
|
|
|
def ignorableWhitespace(self, whitespace):
|
|
pass
|
|
|
|
def processingInstruction(self, target, data):
|
|
pass
|
|
|
|
def flushElementContent(self):
|
|
self.element_content = self.element_content.strip()
|
|
if self.element_content:
|
|
if self.incomplete_tag:
|
|
self.output('>')
|
|
self.incomplete_tag = False
|
|
self.output(xml.sax.saxutils.escape(self.element_content))
|
|
self.element_content = ''
|
|
|
|
|
|
class XmlError(Exception):
|
|
pass
|
|
|
|
|
|
class LayoutError(Exception):
|
|
def __init__(self, msg, locator):
|
|
super(LayoutError, self).__init__(
|
|
'%s:%d:%d: %s' % (locator.getPublicId(), locator.getLineNumber(), locator.getColumnNumber(), msg));
|
|
|
|
|
|
class LayoutChecker(Minifyer):
|
|
VARPATTERN = re.compile('^~scr(0|[1-9][0-9]*)(native[xy]aspect|width|height)~$')
|
|
SHAPES = frozenset(('disk', 'led14seg', 'led14segsc', 'led16seg', 'led16segsc', 'led7seg', 'led8seg_gts1', 'rect'))
|
|
OBJECTS = frozenset(('backdrop', 'bezel', 'cpanel', 'marquee', 'overlay'))
|
|
|
|
def __init__(self, output, **kwargs):
|
|
super(LayoutChecker, self).__init__(output=output, **kwargs)
|
|
self.locator = None
|
|
self.errors = 0
|
|
self.elements = { }
|
|
self.views = { }
|
|
self.referenced = { }
|
|
self.have_bounds = [ ]
|
|
self.have_color = [ ]
|
|
|
|
def formatLocation(self):
|
|
return '%s:%d:%d' % (self.locator.getSystemId(), self.locator.getLineNumber(), self.locator.getColumnNumber())
|
|
|
|
def handleError(self, msg):
|
|
self.errors += 1
|
|
sys.stderr.write('error: %s: %s\n' % (self.formatLocation(), msg))
|
|
|
|
def checkBoundsDimension(self, attrs, name):
|
|
if name in attrs:
|
|
try:
|
|
return float(attrs[name])
|
|
except:
|
|
if not self.VARPATTERN.match(attrs[name]):
|
|
self.handleError('Element bounds attribute %s "%s" is not numeric' % (name, attrs[name]))
|
|
return None
|
|
|
|
def checkBounds(self, attrs):
|
|
if self.have_bounds[-1]:
|
|
self.handleError('Duplicate element bounds')
|
|
else:
|
|
self.have_bounds[-1] = True
|
|
left = self.checkBoundsDimension(attrs, 'left')
|
|
top = self.checkBoundsDimension(attrs, 'top')
|
|
right = self.checkBoundsDimension(attrs, 'right')
|
|
bottom = self.checkBoundsDimension(attrs, 'bottom')
|
|
x = self.checkBoundsDimension(attrs, 'bottom')
|
|
y = self.checkBoundsDimension(attrs, 'bottom')
|
|
width = self.checkBoundsDimension(attrs, 'width')
|
|
height = self.checkBoundsDimension(attrs, 'height')
|
|
if (left is not None) and (right is not None) and (left > right):
|
|
self.handleError('Element bounds attribute left "%s" is greater than attribute right "%s"' % (
|
|
attrs['left'],
|
|
attrs['right']))
|
|
if (top is not None) and (bottom is not None) and (top > bottom):
|
|
self.handleError('Element bounds attribute top "%s" is greater than attribute bottom "%s"' % (
|
|
attrs['top'],
|
|
attrs['bottom']))
|
|
if (width is not None) and (0.0 > width):
|
|
self.handleError('Element bounds attribute width "%s" is negative' % (attrs['width'], ))
|
|
if (height is not None) and (0.0 > height):
|
|
self.handleError('Element bounds attribute height "%s" is negative' % (attrs['height'], ))
|
|
if ('left' not in attrs) and ('x' not in attrs):
|
|
self.handleError('Element bounds has neither attribute left nor attribute x')
|
|
has_ltrb = ('left' in attrs) or ('top' in attrs) or ('right' in attrs) or ('bottom' in attrs)
|
|
has_origin_size = ('x' in attrs) or ('y' in attrs) or ('width' in attrs) or ('height' in attrs)
|
|
if has_ltrb and has_origin_size:
|
|
self.handleError('Element bounds has both left/top/right/bottom and origin/size')
|
|
|
|
def checkColorChannel(self, attrs, name):
|
|
if name in attrs:
|
|
try:
|
|
channel = float(attrs[name])
|
|
if (0.0 > channel) or (1.0 < channel):
|
|
self.handleError('Element color attribute %s "%s" outside valid range 0.0-1.0' % (name, attrs[name]))
|
|
except:
|
|
self.handleError('Element color attribute %s "%s" is not numeric' % (name, attrs[name]))
|
|
|
|
def setDocumentLocator(self, locator):
|
|
self.locator = locator
|
|
super(LayoutChecker, self).setDocumentLocator(locator)
|
|
|
|
def startDocument(self):
|
|
self.in_layout = False
|
|
self.in_element = False
|
|
self.in_view = False
|
|
self.in_shape = False
|
|
self.in_object = False
|
|
self.ignored_depth = 0
|
|
super(LayoutChecker, self).startDocument()
|
|
|
|
def endDocument(self):
|
|
self.locator = None
|
|
self.elements.clear()
|
|
self.views.clear()
|
|
self.referenced.clear()
|
|
del self.have_bounds[:]
|
|
del self.have_color[:]
|
|
super(LayoutChecker, self).endDocument()
|
|
|
|
def startElement(self, name, attrs):
|
|
if 0 < self.ignored_depth:
|
|
self.ignored_depth += 1
|
|
elif not self.in_layout:
|
|
if 'mamelayout' != name:
|
|
self.ignored_depth = 1
|
|
self.handleError('Expected root element mamelayout but found %s' % (name, ))
|
|
else:
|
|
if 'version' not in attrs:
|
|
self.handleError('Element mamelayout missing attribute version')
|
|
else:
|
|
try:
|
|
long(attrs['version'])
|
|
except:
|
|
self.handleError('Element mamelayout attribute version "%s" is not an integer' % (attrs['version'], ))
|
|
self.in_layout = True
|
|
elif self.in_object:
|
|
if 'bounds' == name:
|
|
self.checkBounds(attrs)
|
|
self.ignored_depth = 1
|
|
elif self.in_shape:
|
|
if 'bounds' == name:
|
|
self.checkBounds(attrs)
|
|
elif 'color' == name:
|
|
if self.have_color[-1]:
|
|
self.handleError('Duplicate bounds element')
|
|
else:
|
|
self.have_color[-1] = True
|
|
self.checkColorChannel(attrs, 'red')
|
|
self.checkColorChannel(attrs, 'green')
|
|
self.checkColorChannel(attrs, 'blue')
|
|
self.checkColorChannel(attrs, 'alpha')
|
|
self.ignored_depth = 1
|
|
elif self.in_element:
|
|
if name in self.SHAPES:
|
|
self.in_shape = True
|
|
self.have_bounds.append(False)
|
|
self.have_color.append(False)
|
|
elif 'text' == name:
|
|
if 'string' not in attrs:
|
|
self.handleError('Element bounds missing attribute string')
|
|
if 'align' in attrs:
|
|
try:
|
|
align = long(attrs['align'])
|
|
if (0 > align) or (2 < align):
|
|
self.handleError('Element text attribute align "%s" not in valid range 0-2' % (attrs['align'], ))
|
|
except:
|
|
self.handleError('Element text attribute align "%s" is not an integer' % (attrs['align'], ))
|
|
self.in_shape = True
|
|
self.have_bounds.append(False)
|
|
self.have_color.append(False)
|
|
else:
|
|
self.ignored_depth = 1
|
|
elif self.in_view:
|
|
if name in self.OBJECTS:
|
|
if 'element' not in attrs:
|
|
self.handleError('Element %s missing attribute element', (name, ))
|
|
elif attrs['element'] not in self.referenced:
|
|
self.referenced[attrs['element']] = self.formatLocation()
|
|
self.in_object = True
|
|
self.have_bounds.append(False)
|
|
elif 'screen' == name:
|
|
if 'index' in attrs:
|
|
try:
|
|
index = long(attrs['index'])
|
|
if 0 > index:
|
|
self.handleError('Element screen attribute index "%s" is negative' % (attrs['index'], ))
|
|
except:
|
|
self.handleError('Element screen attribute index "%s" is not an integer' % (attrs['index'], ))
|
|
self.in_object = True
|
|
self.have_bounds.append(False)
|
|
elif 'bounds' == name:
|
|
self.checkBounds(attrs)
|
|
self.ignored_depth = 1
|
|
else:
|
|
self.ignored_depth = 1
|
|
elif 'element' == name:
|
|
if 'name' not in attrs:
|
|
self.handleError('Element element missing attribute name')
|
|
else:
|
|
if attrs['name'] in self.elements:
|
|
self.handleError('Element element has duplicate name (previous %s)' % (self.elements[attrs['name']], ))
|
|
else:
|
|
self.elements[attrs['name']] = self.formatLocation()
|
|
self.in_element = True
|
|
elif 'view' == name:
|
|
if 'name' not in attrs:
|
|
self.handleError('Element view missing attribute name')
|
|
else:
|
|
if attrs['name'] in self.views:
|
|
self.handleError('Element view has duplicate name (previous %s)' % (self.views[attrs['name']], ))
|
|
else:
|
|
self.views[attrs['name']] = self.formatLocation()
|
|
self.in_view = True
|
|
self.have_bounds.append(False)
|
|
else:
|
|
self.ignored_depth = 1
|
|
super(LayoutChecker, self).startElement(name, attrs)
|
|
|
|
def endElement(self, name):
|
|
if 0 < self.ignored_depth:
|
|
self.ignored_depth -= 1
|
|
elif self.in_object:
|
|
self.in_object = False
|
|
self.have_bounds.pop()
|
|
elif self.in_shape:
|
|
self.in_shape = False
|
|
self.have_bounds.pop()
|
|
self.have_color.pop()
|
|
elif self.in_element:
|
|
self.in_element = False
|
|
elif self.in_view:
|
|
self.in_view = False
|
|
self.have_bounds.pop()
|
|
elif self.in_layout:
|
|
for element in self.referenced:
|
|
if element not in self.elements:
|
|
self.handleError('Element "%s" not found (first referenced at %s)' % (element, self.referenced[element]))
|
|
self.in_layout = False
|
|
super(LayoutChecker, self).endElement(name)
|
|
|
|
|
|
def compressLayout(src, dst, comp):
|
|
state = [0, 0]
|
|
def write(block):
|
|
for ch in bytearray(block):
|
|
if 0 == state[0]:
|
|
dst('\t')
|
|
elif 0 == (state[0] % 32):
|
|
dst(',\n\t')
|
|
else:
|
|
dst(', ')
|
|
state[0] += 1
|
|
dst('%3u' % (ch))
|
|
|
|
def output(text):
|
|
block = text.encode('UTF-8')
|
|
state[1] += len(block)
|
|
write(comp.compress(block))
|
|
|
|
error_handler = ErrorHandler()
|
|
content_handler = LayoutChecker(output)
|
|
parser = xml.sax.make_parser()
|
|
parser.setErrorHandler(error_handler)
|
|
parser.setContentHandler(content_handler)
|
|
try:
|
|
parser.parse(src)
|
|
write(comp.flush())
|
|
dst('\n')
|
|
except xml.sax.SAXException as exception:
|
|
print('fatal error: %s' % (exception))
|
|
raise XmlError('Fatal error parsing XML')
|
|
if (content_handler.errors > 0) or (error_handler.errors > 0) or (error_handler.warnings > 0):
|
|
raise XmlError('Error(s) and/or warning(s) parsing XML')
|
|
|
|
return state[1], state[0]
|
|
|
|
|
|
if __name__ == '__main__':
|
|
if len(sys.argv) != 4:
|
|
print('Usage:')
|
|
print(' complay <source.lay> <output.h> <varname>')
|
|
sys.exit(0 if len(sys.argv) <= 1 else 1)
|
|
|
|
srcfile = sys.argv[1]
|
|
dstfile = sys.argv[2]
|
|
varname = sys.argv[3]
|
|
|
|
comp_type = 1
|
|
try:
|
|
dst = open(dstfile,'w')
|
|
dst.write('static const unsigned char %s_data[] = {\n' % (varname))
|
|
byte_count, comp_size = compressLayout(srcfile, lambda x: dst.write(x), zlib.compressobj())
|
|
dst.write('};\n\n')
|
|
dst.write('const internal_layout %s = {\n' % (varname))
|
|
dst.write('\t%d, sizeof(%s_data), %d, %s_data\n' % (byte_count, varname, comp_type, varname))
|
|
dst.write('};\n')
|
|
dst.close()
|
|
except XmlError:
|
|
dst.close()
|
|
os.remove(dstfile)
|
|
sys.exit(2)
|
|
except IOError:
|
|
sys.stderr.write("Unable to open output file '%s'\n" % dstfile)
|
|
os.remove(dstfile)
|
|
dst.close()
|
|
sys.exit(3)
|