├── .gitignore ├── .style.yapf ├── LICENSE ├── README.md ├── VectorDrawable2Svg.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | venv* 2 | -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | based_on_style = google 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alessandro Lucchet 4 | Copyright (c) 2018 Rohan Talip 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VectorDrawable2Svg 2 | 3 | ## Introduction 4 | The VectorDrawable2Svg.py Python script converts Android VectorDrawable `.xml` 5 | files to `.svg` files. 6 | 7 | This repository was forked from 8 | https://gitlab.com/Hyperion777/VectorDrawable2Svg to add handling of indirect 9 | colour references (e.g. in color.xml files) since that repository did not seem 10 | to be actively maintained (based on the unaddressed issues and merge/pull 11 | requests there at the time). 12 | 13 | 14 | ## Usage 15 | ```shell 16 | python3 VectorDrawable2Svg.py a.xml b.xml ... 17 | ``` 18 | 19 | ```shell 20 | ./VectorDrawable2Svg.py --color-xml-file=path/to/color.xml a.xml b.xml ... 21 | ``` 22 | 23 | ```shell 24 | /path/to/VectorDrawable2Svg.py --viewbox-only a.xml b.xml ... 25 | ``` 26 | 27 | The output .svg files are written in the same directory as the .xml files 28 | (currently by simply replacing .xml with .svg in the filename). 29 | 30 | 31 | ## Improvements 32 | This Python script only supports some Android VectorDrawable attributes. 33 | 34 | Suggestions (or merge requests) for improvement are welcome. 35 | 36 | 37 | ## See also 38 | 39 | - [vd2svg](https://github.com/neworld/vd2svg) - written in Kotlin 40 | -------------------------------------------------------------------------------- /VectorDrawable2Svg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | VectorDrawable2Svg 4 | This script converts VectorDrawable .xml files to SVG files. 5 | Initial author: Alessandro Lucchet 6 | Modified by: Rohan Talip 7 | """ 8 | 9 | import argparse 10 | import os.path 11 | from xml.dom.minidom import Document, parse 12 | import traceback 13 | 14 | color_map = {} 15 | 16 | 17 | def read_colors_xml(file_path): 18 | colors_xml = parse(file_path) 19 | resource_node = colors_xml.getElementsByTagName('resources')[0] 20 | for color_node in resource_node.getElementsByTagName('color'): 21 | name = color_node.attributes['name'].value 22 | value = color_node.firstChild.nodeValue 23 | if name in color_map: 24 | raise 'Color ' + name + ' already exists: ' + color_map[name] 25 | color_map[name] = value 26 | 27 | 28 | def get_color(value, depth=1): 29 | prefix = '@color/' 30 | 31 | if value.startswith('#'): 32 | if len(value) == 9: 33 | # This is a hex color value with an alpha channel. 34 | # VectorDrawable files have the alpha at the start and SVG files 35 | # have it at the end. 36 | return '#' + value[3:9] + value[1:3] 37 | return value 38 | 39 | if depth >= 3: 40 | raise 'Depth is >= 3' 41 | 42 | if prefix not in value: 43 | raise '@color not found in ' + value 44 | 45 | name = value.split(prefix)[1] 46 | color = color_map.get(name) 47 | if not color: 48 | return value 49 | 50 | if prefix in color: 51 | return get_color(color, depth + 1) 52 | return color 53 | 54 | 55 | # extracts all paths inside vd_container and add them into svg_container 56 | def convert_paths(vd_container, svg_container, svg_xml): 57 | vd_paths = vd_container.getElementsByTagName('path') 58 | for vd_path in vd_paths: 59 | # only iterate in the first level 60 | if vd_path.parentNode == vd_container: 61 | svg_path = svg_xml.createElement('path') 62 | svg_path.attributes['d'] = vd_path.attributes[ 63 | 'android:pathData'].value 64 | 65 | if vd_path.hasAttribute('android:fillColor'): 66 | svg_path.attributes['fill'] = get_color( 67 | vd_path.attributes['android:fillColor'].value) 68 | else: 69 | svg_path.attributes['fill'] = 'none' 70 | 71 | if vd_path.hasAttribute('android:strokeLineJoin'): 72 | svg_path.attributes['stroke-linejoin'] = vd_path.attributes[ 73 | 'android:strokeLineJoin'].value 74 | if vd_path.hasAttribute('android:strokeLineCap'): 75 | svg_path.attributes['stroke-linecap'] = vd_path.attributes[ 76 | 'android:strokeLineCap'].value 77 | if vd_path.hasAttribute('android:strokeMiterLimit'): 78 | svg_path.attributes['stroke-miterlimit'] = vd_path.attributes[ 79 | 'android:strokeMiterLimit'].value 80 | if vd_path.hasAttribute('android:strokeWidth'): 81 | svg_path.attributes['stroke-width'] = vd_path.attributes[ 82 | 'android:strokeWidth'].value 83 | if vd_path.hasAttribute('android:strokeColor'): 84 | svg_path.attributes['stroke'] = get_color( 85 | vd_path.attributes['android:strokeColor'].value) 86 | 87 | svg_container.appendChild(svg_path) 88 | 89 | 90 | # define the function which converts a vector drawable to a svg 91 | def convert_vector_drawable(vd_file_path, viewbox_only, output_dir): 92 | 93 | # create svg xml 94 | svg_xml = Document() 95 | svg_node = svg_xml.createElement('svg') 96 | svg_xml.appendChild(svg_node) 97 | 98 | # open vector drawable 99 | vd_xml = parse(vd_file_path) 100 | vd_node = vd_xml.getElementsByTagName('vector')[0] 101 | 102 | # setup basic svg info 103 | svg_node.attributes['xmlns'] = 'http://www.w3.org/2000/svg' 104 | if not viewbox_only: 105 | svg_node.attributes['width'] = vd_node.attributes[ 106 | 'android:viewportWidth'].value 107 | svg_node.attributes['height'] = vd_node.attributes[ 108 | 'android:viewportHeight'].value 109 | 110 | svg_node.attributes['viewBox'] = '0 0 {} {}'.format( 111 | vd_node.attributes['android:viewportWidth'].value, 112 | vd_node.attributes['android:viewportHeight'].value) 113 | 114 | # iterate through all groups 115 | vd_groups = vd_xml.getElementsByTagName('group') 116 | for vd_group in vd_groups: 117 | 118 | # create the group 119 | svg_group = svg_xml.createElement('g') 120 | 121 | translate_x = translate_y = 0 122 | 123 | if vd_group.hasAttribute('android:translateX'): 124 | translate_x = vd_group.attributes['android:translateX'].value 125 | 126 | if vd_group.hasAttribute('android:translateY'): 127 | translate_y = vd_group.attributes['android:translateY'].value 128 | 129 | if translate_x or translate_y: 130 | svg_group.attributes['transform'] = 'translate({},{})'.format( 131 | translate_x, translate_y) 132 | 133 | # iterate through all paths inside the group 134 | convert_paths(vd_group, svg_group, svg_xml) 135 | 136 | # append the group to the svg node 137 | svg_node.appendChild(svg_group) 138 | 139 | # iterate through all svg-level paths 140 | convert_paths(vd_node, svg_node, svg_xml) 141 | 142 | # write xml to file 143 | svg_file_path = vd_file_path.replace('.xml', '.svg') 144 | if output_dir: 145 | svg_file_path = os.path.join(output_dir, 146 | os.path.basename(svg_file_path)) 147 | svg_xml.writexml(open(svg_file_path, 'w'), 148 | indent="", 149 | addindent=" ", 150 | newl='\n') 151 | 152 | 153 | def main(): 154 | parser = argparse.ArgumentParser( 155 | description="Convert VectorDrawable .xml files to .svg files.", 156 | formatter_class=argparse.RawDescriptionHelpFormatter, 157 | epilog="e.g. %(prog)s *.xml") 158 | 159 | parser.add_argument("--colors-xml-file", 160 | action="append", 161 | help="A colors.xml file") 162 | parser.add_argument("--output-dir", help="An output directory") 163 | parser.add_argument( 164 | "--viewbox-only", 165 | "--viewBox-only", 166 | help="Only add the viewBox attribute and not width or height", 167 | action="store_true") 168 | parser.add_argument("xml_files", nargs="+", metavar='xml-file') 169 | args = parser.parse_args() 170 | 171 | if args.colors_xml_file: 172 | for colors_xml_file in args.colors_xml_file: 173 | read_colors_xml(colors_xml_file) 174 | 175 | for xml_file in args.xml_files: 176 | print("Converting", xml_file) 177 | try: 178 | convert_vector_drawable(xml_file, args.viewbox_only, 179 | args.output_dir) 180 | except Exception: 181 | print("Failed to convert", xml_file) 182 | traceback.print_exc() 183 | 184 | 185 | if __name__ == "__main__": 186 | main() 187 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flake8 == 3.7.7 2 | yapf == 0.27.0 3 | --------------------------------------------------------------------------------