mirror of
https://github.com/mamedev/mame.git
synced 2024-11-16 07:48:32 +01:00
3a5c5d15c7
appropriate containers, remove misleading const qualifiers, reduce repeated XML walking. (nw) Groups aren't parameterised, so they aren't as useful as they could be (yes, it's on my TODO list). However, it's already useful for putting a common set of elements in multiple views, potentially at different locations/scales. See intlc44.lay and intlc440.lay for examples of the level of copypasta this can eliminate. Be aware that groups with explicit bounds don't clip thair content, it's only used for calucating the transform matrix.
409 lines
15 KiB
Python
409 lines
15 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 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.groups = { }
|
|
self.views = { }
|
|
self.referenced_elements = { }
|
|
self.referenced_groups = { }
|
|
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 checkGroupViewItem(self, name, attrs):
|
|
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_elements:
|
|
self.referenced_elements[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 'group' == name:
|
|
if 'ref' not in attrs:
|
|
self.handleError('Element group missing attribute ref')
|
|
elif attrs['ref'] not in self.referenced_groups:
|
|
self.referenced_groups[attrs['ref']] = self.formatLocation()
|
|
self.in_object = True
|
|
self.have_bounds.append(False)
|
|
elif 'bounds' == name:
|
|
self.checkBounds(attrs)
|
|
self.ignored_depth = 1
|
|
else:
|
|
self.handleError('Encountered unexpected element %s' % (name, ))
|
|
self.ignored_depth = 1
|
|
|
|
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_group = 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.groups.clear()
|
|
self.views.clear()
|
|
self.referenced_elements.clear()
|
|
self.referenced_groups.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_group or self.in_view:
|
|
self.checkGroupViewItem(name, attrs)
|
|
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 'group' == name:
|
|
if 'name' not in attrs:
|
|
self.handleError('Element group missing attribute name')
|
|
else:
|
|
if attrs['name'] in self.groups:
|
|
self.handleError('Element group has duplicate name (previous %s)' % (self.groups[attrs['name']], ))
|
|
else:
|
|
self.groups[attrs['name']] = self.formatLocation()
|
|
self.in_group = True
|
|
self.have_bounds.append(False)
|
|
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)
|
|
elif 'script' == name:
|
|
self.ignored_depth = 1
|
|
else:
|
|
self.handleError('Encountered unexpected element %s' % (name, ))
|
|
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_group:
|
|
self.in_group = False
|
|
self.have_bounds.pop()
|
|
elif self.in_view:
|
|
self.in_view = False
|
|
self.have_bounds.pop()
|
|
elif self.in_layout:
|
|
for element in self.referenced_elements:
|
|
if element not in self.elements:
|
|
self.handleError('Element "%s" not found (first referenced at %s)' % (element, self.referenced_elements[element]))
|
|
for group in self.referenced_groups:
|
|
if group not in self.groups:
|
|
self.handleError('Group "%s" not found (first referenced at %s)' % (group, self.referenced_groups[group]))
|
|
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)
|