From e37e57b63447981a867d60559aa833efcc7766d4 Mon Sep 17 00:00:00 2001
From: ezsh <ezsh.junk@gmail.com>
Date: Sun, 7 Jul 2019 12:18:34 +0200
Subject: [PATCH] Update vector art split scripts

1. Repair identation (my python refused to execute them as they were).
2. Teach normilize_svg to remove explicit "style=fill:" attributes from
elements, controlled by color classes like skin, hair, etc.
3. vector_layer_split moved from inserting vars with sigil for SugarCube
to using "data-transformation" attribute.
4. vector_layer_split output for the 'tw' format became an SVG too, but
with special attributes.
---
 artTools/normalize_svg.py      | 152 +++++++++++++----------
 artTools/vector_layer_split.py | 221 ++++++++++++++++++---------------
 2 files changed, 207 insertions(+), 166 deletions(-)
 mode change 100644 => 100755 artTools/vector_layer_split.py

diff --git a/artTools/normalize_svg.py b/artTools/normalize_svg.py
index e189f619d40..2ee6e332a84 100755
--- a/artTools/normalize_svg.py
+++ b/artTools/normalize_svg.py
@@ -17,71 +17,95 @@ python3 inkscape_svg_fixup.py vector_source.svg
 
 import lxml.etree as etree
 import sys
+import re
+
+color_classes = {
+	'skin', 'head', 'torso', 'boob', 'penis', 'scrotum', 'belly',
+	'areola', 'bellybutton', 'labia', 'hair', 'pubic_hair', 'underarm_hair',
+	'eyebrow_hair', 'shoe', 'shoe_shadow', 'smart_piercing', 'steel_piercing',
+	'steel_chastity', 'outfit_base', 'gag', 'shadow', 'glasses', 'eye', 'sclera'
+}
+
 
 def fix(tree):
-  # know namespaces
-  ns = {
-	'svg' : 'http://www.w3.org/2000/svg',
-	'inkscape' : 'http://www.inkscape.org/namespaces/inkscape'
-  }
-
-  # find document global style definition
-  # mangle it and interpret as python dictionary
-  style_element = tree.find('./svg:style',namespaces=ns)
-  style_definitions = style_element.text
-  pythonic_style_definitions = '{'+style_definitions.\
-  replace('\t.','"').replace('{','":"').replace('}','",').\
-  replace('/*','#')+'}'
-  styles = eval(pythonic_style_definitions)
-
-  # go through all SVG elements
-  for elem in tree.iter():
-	if (elem.tag == etree.QName(ns['svg'], 'g')):
-	  # compare inkscape label with group element ID
-	  l = elem.get(etree.QName(ns['inkscape'], 'label'))
-	  if l:
-		i = elem.get('id')
-		if (i != l):
-		  print("Overwriting ID %s with Label %s..."%(i, l))
-		  elem.set('id', l)
-
-	# clean styles (for easier manual merging)
-	style_string = elem.get('style')
-	if style_string:
-	  split_styles = style_string.strip('; ').split(';')
-	  styles_pairs = [s.strip('; ').split(':') for s in split_styles]
-	  filtered_pairs = [ (k,v) for k,v in styles_pairs if not (
-		k.startswith('font-') or
-		k.startswith('text-') or
-		k.endswith('-spacing') or
-		k in ["line-height", " direction", " writing", " baseline-shift", " white-space", " writing-mode"]
-	  )]
-	  split_styles = [':'.join(p) for p in filtered_pairs]
-	  style_string = ';'.join(sorted(split_styles))
-	  elem.attrib["style"] = style_string
-
-	# remove all style attributes offending class styles
-	s = elem.get('style')
-	c = elem.get('class')
-	if (c and s):
-	  s = s.lower()
-	  c = c.split(' ')[0] # regard main style only
-	  cs = ''
-	  if c in styles:
-		cs = styles[c].strip('; ').lower()
-	  if (c not in styles):
-		print("Object id %s references unknown style class %s."%(i,c))
-	  else:
-		if (cs != s.strip('; ')):
-		  print("Style %s removed from object id %s differed from class %s style %s."%(s,i,c,cs))
-		del elem.attrib["style"]
+	# know namespaces
+	ns = {
+		'svg': 'http://www.w3.org/2000/svg',
+		'inkscape': 'http://www.inkscape.org/namespaces/inkscape'
+	}
+
+	# find document global style definition
+	# mangle it and interpret as python dictionary
+	style_element = tree.find('./svg:style', namespaces=ns)
+	style_definitions = style_element.text
+	pythonic_style_definitions = '{'+style_definitions.\
+		replace('\t.', '"').replace('{', '":"').replace('}', '",').\
+		replace('/*', '#')+'}'
+	styles = eval(pythonic_style_definitions)
+
+	# go through all SVG elements
+	for elem in tree.iter():
+		if (elem.tag == etree.QName(ns['svg'], 'g')):
+			# compare inkscape label with group element ID
+			lbl = elem.get(etree.QName(ns['inkscape'], 'label'))
+			if lbl:
+				i = elem.get('id')
+				if (i != lbl):
+					print("Overwriting ID %s with Label %s..." % (i, lbl))
+					elem.set('id', lbl)
+
+		# clean styles (for easier manual merging)
+		style_string = elem.get('style')
+		if style_string:
+			split_styles = style_string.strip('; ').split(';')
+			styles_pairs = [s.strip('; ').split(':') for s in split_styles]
+			filtered_pairs = [(k, v) for k, v in styles_pairs if not (
+				k.startswith('font-') or
+				k.startswith('text-') or
+				k.endswith('-spacing') or
+				k in ["line-height", " direction", " writing", " baseline-shift", " white-space", " writing-mode"]
+			)]
+			split_styles = [':'.join(p) for p in filtered_pairs]
+			style_string = ';'.join(sorted(split_styles))
+			elem.attrib["style"] = style_string
+
+		# remove all style attributes offending class styles
+		s = elem.get('style')
+		c = elem.get('class')
+		if (c and s):
+			s = s.lower()
+			c = c.split(' ')[0]  # regard main style only
+			classes = c.split(' ')
+			hasColorClass = any(x in color_classes for x in classes)
+			if hasColorClass:
+				s_new = re.sub('fill:#[0-9a-f]+;?', '', s)
+				if s != s_new:
+					print("Explicit fill was removed from style string ({0}) for element  with ID {1} "
+											"because its class ({2}) controls the fill color".format(s, i, c))
+				s = s_new
+			if s == 'style=""':  # the style is empty now
+				del elem.attrib["style"]
+				continue
+			cs = ''
+			if c in styles:
+				cs = styles[c].strip('; ').lower()
+			if (c not in styles):
+				print("Object id %s references unknown style class %s." % (i, c))
+			else:
+				if (cs != s.strip('; ')):
+					print("Style %s removed from object id %s differed from class %s style %s." % (s, i, c, cs))
+					del elem.attrib["style"]
+
+		# remove explicit fill color if element class is one of the color_classes
+
+
 
 if __name__ == "__main__":
-  input_file = sys.argv[1]
-  tree = etree.parse(input_file)
-  fix(tree)
-  # store SVG into file (input file is overwritten)
-  svg = etree.tostring(tree, pretty_print=True)
-  with open(input_file, 'wb') as f:
-	f.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'.encode("utf-8"))
-	f.write(svg)
+	input_file = sys.argv[1]
+	tree = etree.parse(input_file)
+	fix(tree)
+	# store SVG into file (input file is overwritten)
+	svg = etree.tostring(tree, pretty_print=True)
+	with open(input_file, 'wb') as f:
+		f.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'.encode("utf-8"))
+		f.write(svg)
diff --git a/artTools/vector_layer_split.py b/artTools/vector_layer_split.py
old mode 100644
new mode 100755
index abff29fc4a7..4c231df6a40
--- a/artTools/vector_layer_split.py
+++ b/artTools/vector_layer_split.py
@@ -16,109 +16,126 @@ import os
 import copy
 import re
 import normalize_svg
+import argparse
+
+parser = argparse.ArgumentParser(
+	description='Application for splitting groups from one SVG file into separate files.')
+parser.add_argument('-o', '--output', dest='output_dir', required=True,
+                    help='output directory')
+parser.add_argument('-f', '--format', dest='output_format',
+                    choices=['svg', 'tw'], default='svg', help='output format.')
+parser.add_argument('-p', '--prefix', dest='prefix', default='',
+                    help='Prepend this string to result file names')
+parser.add_argument('input_file', metavar='FILENAME', nargs='+',
+                    help='Input SVG file with layers')
+
+args = parser.parse_args()
+output_format = args.output_format
+output_directory = args.output_dir
+
+
+def splitFile(inputFile):
+	tree = etree.parse(inputFile)
+	normalize_svg.fix(tree)
+
+	# prepare output template
+	template = copy.deepcopy(tree)
+	root = template.getroot()
+
+	# remove all svg root attributes except document size
+	for a in root.attrib:
+		if (a != "viewBox"):
+			del root.attrib[a]
+
+	# add placeholder for CSS class (needed for workaround for non HTML 5.1 compliant browser)
+	# if output_format == 'tw':
+		# root.attrib["class"] = "'+_art_display_class+'"
+	ns = {
+		'svg': 'http://www.w3.org/2000/svg',
+		'inkscape': 'http://www.inkscape.org/namespaces/inkscape',
+		'sodipodi': "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
+	}
+
+	# remove all content, including metadata
+	# for twine output, style definitions are removed, too
+	defs = None
+	for e in root:
+		if (e.tag == etree.QName(ns['svg'], 'defs')):
+			defs = e
+		if (e.tag == etree.QName(ns['svg'], 'g') or
+			e.tag == etree.QName(ns['svg'], 'metadata') or
+			e.tag == etree.QName(ns['svg'], 'defs') or
+			e.tag == etree.QName(ns['sodipodi'], 'namedview') or
+			(output_format == 'tw' and e.tag == etree.QName(ns['svg'], 'style'))
+			):
+			root.remove(e)
+	# template preparation finished
+
+	# prepare regex for later use
+	regex_xmlns = re.compile(' xmlns[^ ]+')
+	regex_space = re.compile(r'[>]\s+[<]')
+
+	# find all groups
+	layers = tree.xpath('//svg:g', namespaces=ns)
+	for layer in layers:
+		i = layer.get('id')
+		if (  # disregard non-content groups
+			i.endswith("_") or  # manually suppressed with underscore
+			i.startswith("XMLID") or  # Illustrator generated group
+			i.startswith("g")  # Inkscape generated group
+		):
+			continue
+	# create new canvas
+		output = copy.deepcopy(template)
+	# copy all shapes into template
+		canvas = output.getroot()
+		for e in layer:
+			canvas.append(e)
+		# represent template as SVG (binary string)
+		svg = etree.tostring(output, pretty_print=False)
+		# poor man's conditional defs insertion
+		# TODO: extract only referenced defs (filters, gradients, ...)
+		# TODO: detect necessity by traversing the elements. do not stupidly search in the string representation
+		if ("filter:" in svg.decode('utf-8')):
+			# it seems there is a filter referenced in the generated SVG, re-insert defs from main document
+			canvas.insert(0, defs)
+			# re-generate output
+			svg = etree.tostring(output, pretty_print=False)
+
+		if (output_format == 'tw'):
+			# remove unnecessary attributes
+			# TODO: never generate unnecessary attributes in the first place
+			svg = svg.decode('utf-8')
+			svg = regex_xmlns.sub('', svg)
+			svg = svg.replace(' inkscape:connector-curvature="0"', '')  # this just saves space
+			svg = svg.replace('\n', '').replace('\r', '')  # print cannot be multi-line
+			svg = regex_space.sub('><', svg)  # remove indentation
+			svg = svg.replace('svg:', '')  # svg namespace was removed
+			if ("Boob" in i):  # internal groups are used for scaling
+				svg = svg.replace('<g ', '<g data-transform="boob" ')  # boob art uses the boob scaling
+			elif ("Belly" in i):
+				svg = svg.replace('<g ', '<g data-transform="belly" ')  # belly art uses the belly scaling
+			else:
+				svg = svg.replace('<g ', '<g data-transform="art" ')  # otherwise use default scaling
+		if (not svg.endswith('\n')):
+			svg += '\n'
+		svg = svg.encode('utf-8')
+
+		# save SVG string to file
+		i = layer.get('id')
+		output_path = os.path.join(output_directory, "{0}{1}.svg".format(args.prefix, i))
+		with open(output_path, 'wb') as f:
+			if (output_format == 'svg'):
+				# Header for normal SVG (XML)
+				f.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'.encode("utf-8"))
+				f.write(svg)
+			elif (output_format == 'tw'):
+				f.write(svg)
+
 
-input_file = sys.argv[1]
-output_format = sys.argv[2]
-output_directory = sys.argv[3]
 if not os.path.exists(output_directory):
 	os.makedirs(output_directory)
 
-ns = {
-  'svg' : 'http://www.w3.org/2000/svg',
-  'inkscape' : 'http://www.inkscape.org/namespaces/inkscape',
-  'sodipodi':"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
-}
-
-tree = etree.parse(input_file)
-normalize_svg.fix(tree)
-
-# prepare output template
-template = copy.deepcopy(tree)
-root = template.getroot()
-
-# remove all svg root attributes except document size
-for a in root.attrib:
-  if (a != "viewBox"):
-	del root.attrib[a]
-
-# add placeholder for CSS class (needed for workaround for non HTML 5.1 compliant browser)
-if output_format == 'tw':
-  root.attrib["class"] = "'+_art_display_class+'"
-
-# remove all content, including metadata
-# for twine output, style definitions are removed, too
-defs = None
-for e in root:
-  if (e.tag == etree.QName(ns['svg'], 'defs')):
-	defs = e
-  if (e.tag == etree.QName(ns['svg'], 'g') or
-	  e.tag == etree.QName(ns['svg'], 'metadata') or
-	  e.tag == etree.QName(ns['svg'], 'defs') or
-	  e.tag == etree.QName(ns['sodipodi'], 'namedview') or
-	  (output_format == 'tw' and e.tag == etree.QName(ns['svg'], 'style'))
-	  ):
-	root.remove(e)
-# template preparation finished
-
-# prepare regex for later use
-regex_xmlns = re.compile(' xmlns[^ ]+',)
-regex_space = re.compile('[>][ ]+[<]',)
-
-# find all groups
-layers = tree.xpath('//svg:g',namespaces=ns)
-for layer in layers:
-  i = layer.get('id')
-  if ( # disregard non-content groups
-	i.endswith("_") or # manually suppressed with underscore
-	i.startswith("XMLID") or # Illustrator generated group
-	i.startswith("g") # Inkscape generated group
-  ):
-	continue
-  # create new canvas
-  output = copy.deepcopy(template)
-  # copy all shapes into template
-  canvas = output.getroot()
-  for e in layer:
-	canvas.append(e)
-  # represent template as SVG (binary string)
-  svg = etree.tostring(output, pretty_print=False)
-  # poor man's conditional defs insertion
-  # TODO: extract only referenced defs (filters, gradients, ...)
-  # TODO: detect necessity by traversing the elements. do not stupidly search in the string representation
-  if ("filter:" in svg.decode('utf-8')):
-	# it seems there is a filter referenced in the generated SVG, re-insert defs from main document
-	canvas.insert(0,defs)
-	# re-generate output
-	svg = etree.tostring(output, pretty_print=False)
-
-  if (output_format == 'tw'):
-	# remove unnecessary attributes
-	# TODO: never generate unnecessary attributes in the first place
-	svg = svg.decode('utf-8')
-	svg = regex_xmlns.sub('',svg)
-	svg = svg.replace(' inkscape:connector-curvature="0"','') # this just saves space
-	svg = svg.replace('\n','').replace('\r','') # print cannot be multi-line
-	svg = regex_space.sub('><',svg) # remove indentation
-	svg = svg.replace('svg:','') # svg namespace was removed
-	if ("Boob" in i): # internal groups are used for scaling
-	  svg = svg.replace('<g ','<g transform="\'+_artTransformBoob+\'"') # boob art uses the boob scaling
-	elif ("Belly" in i):
-	  svg = svg.replace('<g ','<g transform="\'+_artTransformBelly+\'"') # belly art uses the belly scaling
-	else:
-	  svg = svg.replace('<g ','<g transform="\'+_art_transform+\'"') # otherwise use default scaling
-	svg = svg.encode('utf-8')
-
-  # save SVG string to file
-  i = layer.get('id')
-  output_path = os.path.join(output_directory,i+'.'+output_format)
-  with open(output_path, 'wb') as f:
-	if (output_format == 'svg'):
-	  # Header for normal SVG (XML)
-	  f.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'.encode("utf-8"))
-	  f.write(svg)
-	elif (output_format == 'tw'):
-	  # Header for SVG in Twine file (SugarCube print statement)
-	  f.write((':: Art_Vector_%s [nobr]\n\n'%(i)).encode("utf-8"))
-	  f.write("<<print '<html>".encode("utf-8"))
-	  f.write(svg)
-	  f.write("</html>' >>".encode("utf-8"))
+for f in args.input_file:
+	splitFile(f)
+
-- 
GitLab