Skip to content

Instantly share code, notes, and snippets.

@john-theo
Last active September 14, 2021 19:50
Show Gist options
  • Select an option

  • Save john-theo/ce178ae4e69047e91d9eda94a7230460 to your computer and use it in GitHub Desktop.

Select an option

Save john-theo/ce178ae4e69047e91d9eda94a7230460 to your computer and use it in GitHub Desktop.
from genericpath import isdir
from bs4 import BeautifulSoup, NavigableString
import re
from svgpathtools import parse_path
import math
STYLE_ATTRS = (
'fill', 'fill-rule', 'stroke', 'stroke-width', 'stroke-linecap',
'stroke-linejoin', 'd', 'width', 'height', 'x', 'y', 'rx', 'ry',
'cx', 'cy', 'r'
)
def children_elements(parent):
return [
x for x in parent.children
if not isinstance(x, NavigableString)
]
def remove_groups(root):
if not root:
return
childrens = children_elements(root)
if len(childrens) == 0:
return
for item in childrens:
remove_groups(item)
if root.name == 'g':
for item in childrens:
for style in STYLE_ATTRS:
if style in root.attrs and style not in item.attrs:
item.attrs[style] = root.attrs[style]
root.unwrap()
def remove_xml_br(xml):
return re.sub(r'\n[ ]*', ' ', xml)
def extract_styles(original):
return {
k: re.sub(r'\n? +', ' ', v) for k, v in original.items()
if k in STYLE_ATTRS
}
def format_svg(xml, fit_viewbox=False, stroke_width=None):
soup = BeautifulSoup(remove_xml_br(xml), "xml")
remove_groups(soup.svg)
x_min, x_max, y_min, y_max = math.inf, 0, math.inf, 0
for path in children_elements(soup.svg):
# Change color
if ('stroke' in path.attrs) and (path.attrs['stroke'] != 'none'):
path.attrs['stroke'] = 'currentColor'
elif ('fill' not in path.attrs) or (path.attrs['fill'] != 'none'):
path.attrs['fill'] = 'currentColor'
# Format path
path.attrs = extract_styles(path.attrs)
if stroke_width and 'stroke-width' in path.attrs:
path.attrs['stroke-width'] = stroke_width
if fit_viewbox \
and ((path.attrs.get('fill', 'none') != 'none')
or (path.attrs.get('stroke', 'none') != 'none')) \
and 'd' in path.attrs:
x1, x2, y1, y2 = parse_path(path.attrs['d']).bbox()
x_min = min(x_min, x1)
x_max = max(x_max, x2)
y_min = min(y_min, y1)
y_max = max(y_max, y2)
# Center paths in viewBox
if fit_viewbox:
width, height = x_max-x_min, y_max-y_min
size = max(width, height)
x0, y0 = (x_max+x_min)/2, (y_max+y_min)/2
x, y, w, h = round(max(0, x0-size/2), 1), round(max(y0 - size/2, 0), 1), round(size, 1), round(size, 1)
viewbox = f'{x} {y} {w} {h}'
else:
viewbox = soup.svg.attrs['viewBox']
soup.svg.attrs = {
'viewBox': viewbox,
'id': 'main',
'xmlns': "http://www.w3.org/2000/svg",
**extract_styles(soup.svg.attrs)
}
if soup.svg.attrs['width']:
del soup.svg.attrs['width']
if soup.svg.attrs['height']:
del soup.svg.attrs['height']
return remove_xml_br(soup.decode_contents(formatter="minimal"))
def format_svg_file(in_file, out_file, fit_viewbox=False, stroke_width=None, in_enc='utf8', out_enc='utf8'):
with open(in_file, 'r', encoding=in_enc) as f:
xml = f.read()
with open(out_file, 'w', encoding=out_enc) as f:
f.write(format_svg(xml, fit_viewbox, stroke_width))
if __name__ == '__main__':
from os import listdir, path
from sys import argv, stderr
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("input", help="input file or directory")
parser.add_argument("-f", "--fit-viewbox", default=False, action="store_true",
help="auto-fit viewbox to content outline")
parser.add_argument("-s", "--stroke-width", default=None, type=float, help="change path stroke width")
parser.add_argument("-o", "--output", help="output file or directory")
if len(argv) <= 1:
parser.print_help(stderr)
exit()
args = parser.parse_args()
in_ = args.input
out_ = args.output
if path.isdir(in_) and path.isdir(out_):
in_files = [path.join(in_, x) for x in listdir(in_)]
out_files = [path.join(out_, x) for x in listdir(in_)]
elif path.isfile(in_):
in_files = [in_]
if path.isdir(out_):
filename = path.basename(in_)
out_files = [path.join(out_, filename)]
elif path.isfile(out_):
out_files = [out_]
else:
raise ValueError("Invalid input/output parameters!")
else:
raise ValueError("Invalid input/output parameters!")
for in_, out_ in zip(in_files, out_files):
if not in_.endswith('.svg'):
continue
try:
format_svg_file(in_, out_, args.fit_viewbox, args.stroke_width)
except Exception as e:
print(in_)
raise e

SVG icons can be downloaded anywhere. When using these SVGs as icons in production, they may not be able to align on the same horizontal line, they can't change color dynamically, along with other problems.

This script does the following things:

  1. Remove undesired attributes in path and svg tags
  2. Remove group content outside, and get rid of g tags
  3. Add id=main to svg tag to support use of an external source
  4. Replace fill and stroke color to currentColor, so the color of the SVG icon can be changed by css
  5. (Optional) Adjust viewBox so it fits the content (0 margin)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment