From a1fa6fd1ae5971f6ec2798cb0888c3f419af657f Mon Sep 17 00:00:00 2001 From: Walter Bender Date: Thu, 26 Dec 2013 15:53:24 +0000 Subject: adding export sugar icon --- (limited to 'util') diff --git a/util/sugariconify.py b/util/sugariconify.py new file mode 100644 index 0000000..55abdf4 --- /dev/null +++ b/util/sugariconify.py @@ -0,0 +1,853 @@ +# -*- coding: utf-8 -*- +#!/usr/bin/env python + +# Copyright (C) 2008 Eben Eliason +# Copyright (C) 2013 Jorge Alberto Gómez López + +# 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 3 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, see . + +import sys +import xml.dom.minidom +import getopt +import re +import os +import string + +HELP = '''\nUsage: sugar-iconify.py [options] input.svg\n +Options:\n + -c\t\tApply default color entities (#666666, #ffffff) to output + -d directory\tThe preferred output directory + -e\t\tDo not insert entities for strokes and fills + -f hex\t\tHex value to replace with fill entity + -g\t\tAutomatically accept guesses for stroke and fill entities + -h\t\tDisplay this help message + -i\t\tInsert "isolated stroke" entities + -m\t\tMultiple export; export top level groups as separate icons + -o\t\tOverwrite the input file; overridden by -m + -p pattern\tOnly export icons whose name matches pattern; for use with -m + -s hex\t\tHex value to replace with stroke entity + -x\t\tOutput example SVGs, for previewing their appearance in Sugar; + \t\tignored with -m + -v\t\tverbose''' + + +class SugarIconify(): + + def __init__(self, command_line=False): + # Declare variables and constants + self.default_stroke_color = '#666666' + self.default_fill_color = '#ffffff' + self.transparent_color = '#00000000' + self.stroke_color = self.default_stroke_color + self.fill_color = self.default_fill_color + self._target_stroke = None + self._target_fill = None + self.stroke_entity = 'stroke_color' + self.fill_entity = 'fill_color' + self.iso_stroke_entity = 'iso_stroke_color' + + self.output_path = '' + self.pattern = '' + self.entities_passed = 0 + self.use_default_colors = False + self.confirm_guess = True + self.use_entities = True + self.multiple = False + self.verbose = False + self.overwrite_input = False + self.output_examples = False + self.use_iso_strokes = False + + if command_line: + self._parse_command_line() + + def usage(self): + # Define help output + print HELP + + def _parse_command_line(self): + ''' Try to make sense of the command-line arguments. ''' + try: + opts, arg = getopt.getopt(sys.argv[1:], 's:f:gcd:imp:oehvx', + ['stroke=', 'fill=', 'guess', 'help', + 'overwrite', 'verbose']) + except: + self.usage() + sys.exit(2) + + if len(arg) < 1: + self.usage() + sys.exit(2) + + # Interpret arguments + for o, a in opts: + if o in ['-s', '--stroke']: + self.set_stroke_color(a) + elif o in ['-f', '--fill']: + self.set_fill_color(a) + elif o in ['-g', '--guess']: + self.set_confirm_guess(False) + elif o == '-c': + self.set_use_default_colors(True) + elif o in ['-o', '--overwrite']: + self.set_overwrite_input(True) + elif o == '-d': + self.set_output_path(a) + elif o == '-e': + self.set_use_entities(False) + elif o in ['-v', 'verbose']: + self.set_verbose(True) + elif o == '-p': + self.set_pattern(a) + elif o in ['-h', '--help']: + usage() + sys.exit(2) + elif o == '-m': + self.set_multiple(True) + elif o == '-x': + self.set_output_examples(True) + elif o == '-i': + self.set_use_iso_strokes(True) + + self.iconify(arg[0]) + + def rgb_to_hex(self, rgb_str): + s = re.sub(r'.*rgb\(([^)]*).*', r'\1', rgb_str) + percent_list = s.split(',') + hex_str = '#' + for value in percent_list: + hex_str += self.percent_to_hex(value) + return hex_str + + def percent_to_hex(self, num): + number = float(num.strip()[:-1]) + decimal = (number*255)/100 + decimal = int(round(decimal, 0)) + hex_val = hex(decimal).split('x')[1] + if len(hex_val) == 1: + hex_val = '0' + hex_val + return hex_val + + def set_stroke_color(self, s=None): + if s is not None: + if 'rgb' in s.lower(): + self.stroke_color = self.rgb_to_hex(s) + self._target_stroke = s + else: + self.stroke_color = '#' + s.lstrip('#').lower() + self.entities_passed += 1 + + def set_fill_color(self, f=None): + if f is not None: + if 'rgb' in f.lower(): + self.fill_color = self.rgb_to_hex(f) + self._target_fill = f + else: + self.fill_color = '#' + f.lstrip('#').lower() + self.entities_passed += 1 + + def set_confirm_guess(self, g=False): + self.confirm_guess = g + + def set_use_default_colors(self, c=False): + self.use_default_colors = c + + def set_overwrite_input(self, o=False): + self.overwrite_input = o + + def set_output_path(self, d=None): + if d is not None: + self.output_path = d.rstrip('/') + '/' + + def set_use_entity(self, e=False): + self.use_entities = e + + def set_verbose(self, v=False): + self.verbose = v + + def set_pattern(self, p=None): + if p is not None: + self.pattern = p + + def set_multiple(self, m=False): + self.multiple = m + + def set_output_examples(self, x=False): + self.output_examples = x + + def set_use_iso_strokes(self, i=False): + self.use_iso_strokes = i + + def iconify(self, file_path): + # Isolate important parts of the input path + self.svgfilepath = file_path + self.svgdirpath, self.sep, self.svgfilename = \ + self.svgfilepath.rpartition('/') + svgbasename = re.sub(r'(.*)\.([^.]+)', r'\1', self.svgfilename) + + # Load the SVG as text + try: + self.svgfile = open(self.svgfilepath, 'r') + except: + sys.exit('Error: Could not locate ' + self.svgfilepath) + + try: + self.svgtext = self.svgfile.read() + self.svgfile.close() + except: + self.svgfile.close() + sys.exit('Error: Could not read ' + self.svgfilepath) + + # Determine the creator of the SVG (we only care about + # Inkscape and Illustrator) + self.creator = 'unknown' + + if re.search('illustrator', self.svgtext, re.I): + self.creator = 'illustrator' + elif re.search('inkscape', self.svgtext, re.I): + self.creator = 'inkscape' + + if self.verbose: + print 'The self.creator of this svg is ' + self.creator + '.' + + # Hack the entities into the readonly DTD + if self.use_entities: + + # Before replacing them, we read the stroke/fill values + # out, should they have previously been defined, to prevent + # needing to make guesses for them later + self.stroke_match = re.search(r'stroke_color\s*\"([^"]*)\"', + self.svgtext) + self.fill_match = re.search(r'fill_color\s*\"([^"]*)\"', + self.svgtext) + + if self.stroke_match is not None: + self.stroke_color = self.stroke_match.group(1).lower() + self.entities_passed += 1 + if self.fill_match is not None: + self.fill_color = self.fill_match.group(1).lower() + self.entities_passed += 1 + + # Define the entities + if self.fill_match and self.stroke_match: + self.entities = '\t\n' + self.entities += '\t\n' + if self.use_iso_strokes: + self.entities += '\t\n' + else: + self.entities = '\t\n' + self.entities += '\t\n' + if self.use_iso_strokes: + self.entities += '\t\n' + + # For simplicity, we simply replace the entire entity + # declaration block; this obviously would remove any other + # custom self.entities declared within the SVG, but we + # assume that's an extreme edge case + + self.svgtext, self.n = \ + re.subn(r'(\[]*)(\[[^\]]*\])*\>', + r'\1 \n[\n' + self.entities + ']>\n', self.svgtext) + + # Add a doctype if none already exists, adding the + # appropriate self.entities as well + if self.n == 0: + self.svgtext, self.n = \ + re.subn('\n\n\ + \n\ + \n\ + \t\n\ + \tSugar Icon Preview: ~~~\n\ + \t\n\ + \t\n\ + \n\ + \n\ + \t\t
    \n\ + \t\t

  • stroke\n\ + \t\t

  • fill\n\ + \t\t

  • both\n\ + \t\t
\n\ + \t\t
\n\ + \t\t\t

Icon Validation

\n\ + \t\t\t
    \n\ + \t\t\t\t
  • Ensure that your icons appear to be centered within their boxes.\n\ + \t\t\t\t\tIf they appear off-center or cropped, you may have created your icon on canvas other than the required 55px square.\n\ + \t\t\t\t\tClick to toggle the 55px canvas border.\n\ + \t\t\t\t
  • If your icon appears off-center but has the correct 55px canvas, it may simply have uneven visual balance.\n\ + \t\t\t\t\tThis means, though it may be technically centered, differences in the distribution of \"mass\" cause it to appear otherwise. \n\ + \t\t\t\t\tTry shifting the icon slightly on your canvas, while ensuring that you don't accidentally exceed the 55px boundary.\n\ + \t\t\t\t\n\ + \t\t\t\t
  • Ensure that the first two icons appear entirely in gray, and that all of the third icon is colored blue and green, with the latter being the fill color.\n\ + \t\t\t\t\tIf any fail to meet these requirements, your icon does not have proper stroke and/or fill self.entities defined.\n\ + \t\t\t\t\tInvestigate the -s and -f options of sugar-iconify, and be sure that your input self.svg doesn't have extra colors in its palette.\n\ + \t\t\t\t
  • Ensure that your icon reads clearly when viewed only as strokes.\n\ + \t\t\t\t\tThis visual style will be used to represent activities/objects which are inactive, or uninstantiated.\n\ + \t\t\t\t\tConsider applying outlining strokes to any filled shapes that do not already have them.\n\ + \t\t\t\t
  • Ensure that your icon reads clearly when viewed only as fills.\n\ + \t\t\t\t\tThis visual style will be used for representing activity types within other icons, such as invitations, transfers, and objects. \n\ + \t\t\t\t\tIf you have strokes which are isolated from fills, neither outlining them nor sitting against a filled background, please \n\ + \t\t\t\t\tinvestigate the -i option of the sugar-iconify script.\n\ + \t\t\t
\n\ + \t\t\tFor more information, please see the OLPC wiki page on making sugar icons.\n\ + \t\t
\n\ + \n\ + \n\ + " + + # Finally, do the icon conversion and export + if self.multiple: + # Export each icon as a separate file by top level group + n_icons_exported = 0 + n_warnings = 0 + for icon in icons: + + try: + # Skip whitespace and unnamed icons + if icon.localName == 'g' and icon.attributes: + + icon_name = '' + try: + if self.creator == 'inkscape' and \ + icon.attributes.getNamedItem('inkscape:label'): + icon_name = icon.attributes.getNamedItem( + 'inkscape:label').nodeValue + else: + icon_name = \ + icon.attributes.getNamedItem('id').nodeValue + except: + pass + + # Skip the template layers + if not icon_name.startswith('_'): + + # Skip non-matches + if self.pattern == '' or \ + re.search(self.pattern, icon_name): + + if self.verbose: + print '\nExporting ' + icon_name + \ + '.self.svg...' + icon_xml = xml.dom.minidom.Document(); + + # Construct the self.svg + icon_xml.appendChild(doctype) + icon_xml.appendChild(self.svg.cloneNode(0)) + + icon_xml.childNodes[1].appendChild(icon) + icon_xml.childNodes[1].childNodes[0].setAttribute('display', 'block') + + if self.use_entities: + strokes_replaced, fills_replaced = \ + self.replaceEntities(icon_xml.childNodes[1]) + + if not strokes_replaced and not fills_replaced: + print 'Warning: no entity replacements were made in %s' % icon_name + elif not strokes_replaced: + print 'Warning: no stroke entity replacements were made in %s' % icon_name + elif not fills_replaced: + print 'Warning: no fill entity replacements were made in %s' % icon_name + + if not strokes_replaced or not fills_replaced: + n_warnings += 1 + + # Write the file + try: + f = open(self.output_path + icon_name + \ + '.self.svg', 'w') + except: + sys.exit('Error: Could not locate directory ' + self.output_path) + + try: + # Had to hack here to remove the + #automatic encoding of '&' by + #toxml() in entity refs I'm sure + #there is a way to prevent need + #for this if I knew the XML DOM + #better + icon_svgtext = icon_xml.toxml() + icon_svgtext = re.sub('&', '&', + icon_svgtext) + if not self.use_default_colors: + icon_svgtext = re.sub(r'ENTITY self.stroke_color "[^"]*"', r'ENTITY self.stroke_color "' + self.stroke_color + '"', icon_svgtext) + icon_svgtext = re.sub(r'ENTITY self.fill_color "[^"]*"', r'ENTITY self.fill_color "' + self.fill_color + '"', icon_svgtext) + f.write(icon_svgtext) + f.close() + except: + sys.exit('Error: Could not write file ' + icon_name + '.self.svg') + + n_icons_exported += 1 + except: + # Catch any errors we may have missed, so the rest + # of the icons can export normally + if(icon_name): + print 'Error: Could not export' + icon_name + \ + '.self.svg' + + if self.verbose: + if n_icons_exported == 1: + print 'Successfully exported 1 icon' + else: + print 'Successfully exported %d icons' % n_icons_exported + + if n_warnings == 1: + print 'Warnings were reported for 1 icon' + elif n_warnings > 1: + print 'Warnings were reported for %d icons' % n_warnings + + else: + # Output a single converted icon + if not self.overwrite_input: + outfilename = re.sub(r'(.*\.)([^.]+)', r'\1sugar.\2', + self.svgfilename) + if self.verbose: + print 'Exporting ' + outfilename + ' ...' + else: + outfilename = self.svgfilename + if self.verbose: + print 'Overwriting ' + outfilename + ' ...' + + # Remove the template layers + for node in self.svg.childNodes: + + # Only check named nodes + if node.localName == 'g' and node.attributes: + try: + if self.creator == 'inkscape' and \ + node.attributes.getNamedItem('inkscape:label'): + node_name = node.attributes.getNamedItem('inkscape:label').nodeValue + else: + node_name = node.attributes.getNamedItem('id').nodeValue + + if node_name.startswith('_'): + node.parentNode.removeChild(node) + except: + pass + + if self.use_entities: + strokes_replaced, fills_replaced = \ + self.replaceEntities(self.svgxml) + if not strokes_replaced and not fills_replaced: + print 'Warning: no entity replacements were made' + elif not strokes_replaced: + print 'Warning: no stroke entity replacements were made' + elif not fills_replaced: + print 'Warning: no fill entity replacements were made' + + if self.use_iso_strokes: + strokes_fixed = self.fix_isolated_strokes(self.svgxml) + if strokes_fixed > 0 and self.verbose: + print "%d isolated strokes fixed" % strokes_fixed + + # Create the output file(s) + if self.output_examples: + + example_path = self.output_path + \ + re.sub(r'(.*\.)([^.]+)', r'\1preview', + self.svgfilename) + '/' + try: + os.mkdir(example_path) + except: + pass + + try: + f = open(example_path + 'preview.html', 'w') + except: + print "Error: could not create HTML preview file" + + try: + f.write(re.sub(r'~~~', svgbasename, self.previewHTML)) + f.close() + except: + sys.exit('Error: could not write to HTML preview file') + + example_colors = [(self.default_stroke_color, '#FFFFFF', + self.default_stroke_color), + ('#FFFFFF', self.default_stroke_color, + self.default_stroke_color), + ('#0000AA', '#00DD00', '#0000AA')] + example_filenames = [re.sub(r'(.*\.)([^.]+)', r'\1stroke.\2', + self.svgfilename), + re.sub(r'(.*\.)([^.]+)', r'\1fill.\2', + self.svgfilename), + re.sub(r'(.*\.)([^.]+)', r'\1both.\2', + self.svgfilename) ] + + icon_svgtext = self.svgxml.toxml() + icon_svgtext = re.sub('&', '&', icon_svgtext) + + for i in range(0, len(example_filenames)): + try: + f = open(example_path + example_filenames[i], 'w') + except: + sys.exit('Error: Could not save to ' + example_path + \ + example_filenames[i]) + try: + icon_svgtext = re.sub(r'ENTITY self.stroke_color "[^"]*"', r'ENTITY self.stroke_color "' + example_colors[i][0] + '"', icon_svgtext) + icon_svgtext = re.sub(r'ENTITY self.fill_color "[^"]*"', r'ENTITY self.fill_color "' + example_colors[i][1] + '"', icon_svgtext) + if self.use_iso_strokes: + icon_svgtext = re.sub(r'ENTITY iso_stroke_color "[^"]*"', r'ENTITY iso_stroke_color "' + example_colors[i][2] + '"', icon_svgtext) + f.write(icon_svgtext) + f.close() + except: + sys.exit('Error: Could not write file ' + \ + self.output_path + example_filenames[i]) + + try: + f = open(self.output_path + outfilename, 'w') + except: + sys.exit('Error: Could not save to ' + self.output_path + \ + outfilename) + + try: + icon_svgtext = self.svgxml.toxml() + icon_svgtext = re.sub('&', '&', icon_svgtext) + if not self.use_default_colors: + icon_svgtext = re.sub(r'ENTITY self.stroke_color "[^"]*"', r'ENTITY self.stroke_color "' + self.stroke_color + '"', icon_svgtext) + icon_svgtext = re.sub(r'ENTITY self.fill_color "[^"]*"', r'ENTITY self.fill_color "' + self.fill_color + '"', icon_svgtext) + if self.use_iso_strokes: + icon_svgtext = re.sub(r'ENTITY iso_stroke_color "[^"]*"', r'ENTITY iso_stroke_color "' + self.stroke_color + '"', icon_svgtext) + f.write(icon_svgtext) + f.close() + + except: + sys.exit('Error: Could not write file ' + self.output_path + \ + outfilename) + + # Define utility functions + def getStroke(self, node): + s = node.getAttribute('stroke') + if s: + return s.lower() + else: + if re.search(r'stroke:', node.getAttribute('style')): + s = re.sub(r'.*stroke:\s*(#*[^;]*).*', r'\1', node.getAttribute('style')) + s2 = re.sub(r'.*stroke:\s*rgb\(([^)]*).*', r'\1', node.getAttribute('style')) + if 'rgb' not in s: + return s.lower() + else: + percent_list = s2.split(',') + hex_str = '#' + for value in percent_list: + hex_str += self.percent_to_hex(value) + return hex_str + else: + return 'none' + + def setStroke(self, node, value): + s = node.getAttribute('stroke') + if s: + node.setAttribute('stroke', value) + else: + s = re.sub(r'stroke:\s*[^;]*', 'stroke:' + value, + node.getAttribute('style')) + node.setAttribute('style', s) + + def getFill(self, node): + f = node.getAttribute('fill') + if f: + return f.lower() + else: + if re.search(r'fill:', node.getAttribute('style')): + f = re.sub(r'.*fill:\s*(#*[^;]*).*', r'\1', + node.getAttribute('style')) + f2 = re.sub(r'.*fill:\s*rgb\(([^)]*).*', r'\1', + node.getAttribute('style')) + if 'rgb' not in f: + return f.lower() + else: + percent_list = f2.split(',') + hex_str = '#' + for value in percent_list: + hex_str += self.percent_to_hex(value) + return hex_str + else: + return 'none' + + def setFill(self, node, value): + f = node.getAttribute('fill') + if f: + node.setAttribute('fill', value) + else: + s = re.sub(r'fill:\s*[^;]*', 'fill:' + value, + node.getAttribute('style')) + node.setAttribute('style', s) + + def replaceEntities(self, node, indent=''): + + strokes_replaced = 0 + fills_replaced = 0 + + if node.localName: + str = indent + node.localName + + if node.nodeType == 1: # Only element nodes have attrs + + # Replace self.entities for matches + if self.getStroke(node) == self.stroke_color: + self.setStroke(node, self.stroke_entity) + strokes_replaced += 1 + + if self.getStroke(node) == self.fill_color: + self.setStroke(node, self.fill_entity) + strokes_replaced += 1 + + if self.getFill(node) == self.fill_color: + self.setFill(node, self.fill_entity) + fills_replaced += 1 + + if self.getFill(node) == self.stroke_color: + self.setFill(node, self.stroke_entity) + fills_replaced += 1 + + str = str + " (" + self.getStroke(node) + ", " + \ + self.getFill(node) + ")" + if self.verbose: + print str + + # Recurse on DOM + for n in node.childNodes: + sr, fr = self.replaceEntities(n, indent + " ") + strokes_replaced += sr + fills_replaced += fr + + # Return the number of replacements made + return (strokes_replaced, fills_replaced) + + def fix_isolated_strokes(self, node): + strokes_fixed = 0 + # Recurse on DOM + last_n = None + for n in node.childNodes: + sf = self.fix_isolated_strokes(n) + strokes_fixed += sf + + if node.nodeType == 1: # Only element nodes have attrs + + # Find strokes with no associated fill + if self.getStroke(node) != 'none' and self.getFill(node) == 'none': + strokes_fixed += 1 + self.setStroke(node, "&iso_stroke_color;") + + # Return the number of strokes fixed + return strokes_fixed + + # These functions attempt to guess the hex values for the stroke + # and fill self.entities + + def getColorPairs(self, node, pairs=[]): + if node.nodeType == 1: + + # Skip masks + if node.localName == 'mask': + return pairs + + node_name = '' + try: + if self.creator == 'inkscape' and \ + node.attributes.getNamedItem('inkscape:label'): + node_name = node.attributes.getNamedItem('inkscape:label').nodeValue + else: + node_name = node.attributes.getNamedItem('id').nodeValue + except: + pass + + # Skip the template layers + if node_name.startswith('_'): + return pairs + + pair = (self.getStroke(node), self.getFill(node)) + if pair[0] != pair[1]: + pairs.append(pair) + + # Recurse on DOM + for n in node.childNodes: + self.getColorPairs(n, pairs) + + return pairs + + def guessEntities(self, node): + guesses = self.getColorPairs(node) + + if self.stroke_color is not None: + stroke_guess = self.stroke_color + else: + stroke_guess = 'none' + + if self.stroke_color is not None: + fill_guess = self.fill_color + else: + fill_guess = 'none' + + for guess in guesses: + if stroke_guess == 'none': + stroke_guess = guess[0] + if fill_guess == 'none' and stroke_guess != guess[1]: + fill_guess = guess[1] + if guess[0] == fill_guess and guess[1] != 'none': + fill_guess = stroke_guess + stroke_guess = guess[0] + if fill_guess == 'none': + fill_guess = guess[1] + + return (stroke_guess, fill_guess) + + +if __name__ == '__main__': + SugarIconify(command_line=True) -- cgit v0.9.1