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